diff --git a/Cargo.lock b/Cargo.lock index a654e155..69d4c448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,10 +685,12 @@ dependencies = [ "eflint-to-json", "enum-debug", "error-trace", + "gethostname", "hex-literal", "human-panic 2.0.2", "humanlog", "humantime", + "indoc", "jsonwebtoken", "lazy_static", "log", @@ -1884,6 +1886,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix", + "windows-targets 0.52.6", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2583,6 +2595,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "instant" version = "0.1.13" @@ -4193,6 +4211,7 @@ dependencies = [ "const_format", "enum-debug", "futures", + "indoc", "jsonwebtoken", "log", "num-traits", diff --git a/brane-ctl/Cargo.toml b/brane-ctl/Cargo.toml index e10e976d..8dbbb5a2 100644 --- a/brane-ctl/Cargo.toml +++ b/brane-ctl/Cargo.toml @@ -28,25 +28,26 @@ diesel_migrations = "2.2.0" dirs = "5.0.1" dotenvy = "0.15.0" eflint-to-json = { git = "https://github.com/epi-project/policy-reasoner" } -enum-debug.workspace = true -error-trace.workspace = true +enum-debug = { workspace = true, features = ["derive"] } +error-trace = { workspace = true } # env_logger = "0.10" +gethostname = "0.5.0" hex-literal = "0.4.0" humanlog.workspace = true -humantime = "2.1.0" human-panic = "2.0.0" +humantime = "2.1.0" jsonwebtoken = "9.2.0" lazy_static = "1.4.0" log = "0.4.22" names.workspace = true policy = { git = "https://github.com/epi-project/policy-reasoner" } -srv = { git = "https://github.com/epi-project/policy-reasoner" } rand = "0.8.5" reqwest = { version = "0.11.27" } serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" serde_yaml = { version = "0.0.10", package = "serde_yml" } shlex = "1.1.0" +srv = { git = "https://github.com/epi-project/policy-reasoner" } tempfile = "3.10.1" tokio = { version = "1.38.0", features = [] } @@ -55,6 +56,7 @@ brane-shr = { path = "../brane-shr" } brane-tsk = { path = "../brane-tsk" } specifications = { path = "../specifications" } clap_complete = "4.5.8" +indoc = "2.0.5" # Hacky indeed but necessary to dodge OpenSSL linking, which we need to make cross-compilation _so_ much easier diff --git a/brane-ctl/src/generate.rs b/brane-ctl/src/generate.rs index 1238db95..ca3c73dc 100644 --- a/brane-ctl/src/generate.rs +++ b/brane-ctl/src/generate.rs @@ -586,7 +586,7 @@ struct CfsslCsrKey { /***** LIBRARY *****/ -/// Handles generating a new `node.yml` config file for a central _or_ worker node. +/// Handles creating a new `node.yml` config file for a central, worker, _or_ proxy node. /// /// # Arguments /// - `path`: The path to write the central node.yml to. @@ -600,14 +600,39 @@ struct CfsslCsrKey { /// /// # Errors /// This function may error if I/O errors occur while writing the file. -pub fn node( +pub fn node ( path: impl Into, hosts: Vec>, fix_dirs: bool, config_path: impl Into, command: GenerateNodeSubcommand, ) -> Result<(), Error> { - let path: PathBuf = path.into(); + // TODO: AsRef Path is probably nicer + let path = path.into(); + let node = generate_node(hosts, fix_dirs, config_path, command)?; + write_node(node, &path) +} + +/// Handles creating a new NodeConfig object for a central, worker, _or_ proxy node. +/// +/// # Arguments +/// - `path`: The path to write the central node.yml to. +/// - `hosts`: List of additional hostnames to set in the launched containers. +/// - `fix_dirs`: if true, will generate missing directories instead of complaining. +/// - `config_path`: The path to the config directory that other paths may use as their base. +/// - `command`: The GenerateSubcommand that contains the specific values to write, as well as whether to write a central or worker node. +/// +/// # Returns +/// A NodeConfig object constructed using the provided parameters +/// +/// # Errors +/// This function may error if I/O errors occur while writing the file. +pub fn generate_node( + hosts: Vec>, + fix_dirs: bool, + config_path: impl Into, + command: GenerateNodeSubcommand, +) -> Result { let config_path: PathBuf = config_path.into(); info!("Generating node.yml for a {}...", match &command { GenerateNodeSubcommand::Central { .. } => { @@ -654,12 +679,15 @@ pub fn node( plr_port, } => { // Remove any scheme, paths, ports, whatever from the hostname - let mut hostname: &str = &hostname; - if let Some(pos) = hostname.find("://") { - hostname = &hostname[pos + 3..]; - } - hostname = hostname.split(':').next().unwrap(); - hostname = hostname.split('/').next().unwrap(); + let hostname = match hostname.split_once("://") { + Some((_scheme, hostname)) => hostname, + None => &hostname + }; + + let hostname = match hostname.split_once([':', '/']) { + Some((hostname, _port_uri)) => hostname, + None => hostname, + }; // Resolve any path depending on the '$CONFIG' let infra: PathBuf = resolve_config_path(infra, &config_path); @@ -667,6 +695,8 @@ pub fn node( let certs: PathBuf = resolve_config_path(certs, &config_path); // Ensure the directory structure is there + // TODO: Does not really seem like the responsibility of generating a node as if these + // dont exist we probably have bigger problems. ensure_dir_of(&infra, fix_dirs)?; ensure_dir_of(&proxy, fix_dirs)?; ensure_dir(&certs, fix_dirs)?; @@ -754,12 +784,15 @@ pub fn node( chk_port, } => { // Remove any scheme, paths, ports, whatever from the hostname - let mut hostname: &str = &hostname; - if let Some(pos) = hostname.find("://") { - hostname = &hostname[pos + 3..]; - } - hostname = hostname.split(':').next().unwrap(); - hostname = hostname.split('/').next().unwrap(); + let hostname = match hostname.split_once("://") { + Some((_scheme, hostname)) => hostname, + None => &hostname + }; + + let hostname = match hostname.split_once([':', '/']) { + Some((hostname, _port_uri)) => hostname, + None => hostname, + }; // Resolve the service names let prx_name: String = prx_name.replace("$LOCATION", &location_id); @@ -855,12 +888,15 @@ pub fn node( // Generate the proxy node GenerateNodeSubcommand::Proxy { hostname, proxy, certs, prx_name, prx_port } => { // Remove any scheme, paths, ports, whatever from the hostname - let mut hostname: &str = &hostname; - if let Some(pos) = hostname.find("://") { - hostname = &hostname[pos + 3..]; - } - hostname = hostname.split(':').next().unwrap(); - hostname = hostname.split('/').next().unwrap(); + let hostname = match hostname.split_once("://") { + Some((_scheme, hostname)) => hostname, + None => &hostname + }; + + let hostname = match hostname.split_once([':', '/']) { + Some((hostname, _port_uri)) => hostname, + None => hostname, + }; // Resolve any path depending on the '$CONFIG' let proxy: PathBuf = resolve_config_path(proxy, &config_path); @@ -892,26 +928,29 @@ pub fn node( }, }; + // Done + Ok(node_config) +} + +/// Writes a node config to a file. Appends stuff like a header. +/// TODO: This needs to be a method on NodeConfig +fn write_node(node: NodeConfig, path: impl AsRef) -> Result<(), Error> { + let path = path.as_ref(); // Open the file and write a header to it debug!("Writing to '{}'...", path.display()); - let mut handle: File = match File::create(&path) { - Ok(handle) => handle, - Err(err) => { - return Err(Error::FileCreateError { what: "node.yml", path, err }); - }, - }; + let mut handle = File::create(&path) + .map_err(|err| Error::FileCreateError { what: "node.yml", path: path.to_owned(), err })?; // Write the top comment header thingy - if let Err(err) = write_node_header(&mut handle) { - return Err(Error::FileHeaderWriteError { what: "infra.yml", path, err }); - } + write_node_header(&mut handle) + .map_err(|err| Error::FileHeaderWriteError { what: "infra.yml", path: path.to_owned(), err })?; + // Write the file itself - if let Err(err) = node_config.to_writer(handle, true) { - return Err(Error::FileBodyWriteError { what: "infra.yml", path, err }); - } + node.to_writer(handle, true) + .map_err(|err| Error::FileBodyWriteError { what: "infra.yml", path: path.to_owned(), err })?; - // Done println!("Successfully generated {}", style(path.display().to_string()).bold().green()); + Ok(()) } @@ -1176,7 +1215,7 @@ pub fn infra( job_ports: Vec R let (_dir, migrations): (TempDir, FileBasedMigrations) = { // Prepare the input URL and output directory let url = format!("https://api.github.com/repos/epi-project/policy-reasoner/tarball/{branch}"); - let dir = match TempDir::new() { - Ok(dir) => dir, - Err(err) => { - return Err(Error::TempDirError { err }); - }, - }; + + let dir = TempDir::new().map_err(|err| Error::TempDirError { err })?; // Download the file let tar_path: PathBuf = dir.path().join("repo.tar.gz"); let dir_path: PathBuf = dir.path().join("repo"); - if let Err(err) = brane_shr::fs::download_file_async(&url, &tar_path, DownloadSecurity { checksum: None, https: true }, None).await { - return Err(Error::RepoDownloadError { repo: url, target: dir_path, err }); - } - if let Err(err) = brane_shr::fs::unarchive_async(&tar_path, &dir_path).await { - return Err(Error::RepoUnpackError { tar: tar_path, target: dir_path, err }); - } + + brane_shr::fs::download_file_async(&url, &tar_path, DownloadSecurity { checksum: None, https: true }, None) + .await + .map_err(|err| Error::RepoDownloadError { repo: url, target: dir_path.clone(), err })?; + + brane_shr::fs::unarchive_async(&tar_path, &dir_path).await.map_err(|err| Error::RepoUnpackError { + tar: tar_path, + target: dir_path.clone(), + err, + })?; + // Resolve that one weird folder in there - let dir_path: PathBuf = match brane_shr::fs::recurse_in_only_child_async(&dir_path).await { - Ok(path) => path, - Err(err) => { - return Err(Error::RepoRecurseError { target: dir_path, err }); - }, - }; + let dir_path: PathBuf = + brane_shr::fs::recurse_in_only_child_async(&dir_path).await.map_err(|err| Error::RepoRecurseError { target: dir_path, err })?; // Read that as the migrations - let migrations: FileBasedMigrations = match FileBasedMigrations::find_migrations_directory_in_path(&dir_path) { - Ok(migrations) => migrations, - Err(err) => return Err(Error::MigrationsRetrieve { path: dir_path, err }), - }; + let migrations: FileBasedMigrations = + FileBasedMigrations::find_migrations_directory_in_path(&dir_path).map_err(|err| Error::MigrationsRetrieve { path: dir_path, err })?; + (dir, migrations) }; diff --git a/brane-ctl/src/main.rs b/brane-ctl/src/main.rs index 1788a36f..4d46a853 100644 --- a/brane-ctl/src/main.rs +++ b/brane-ctl/src/main.rs @@ -98,7 +98,7 @@ async fn main() { GenerateSubcommand::PolicyDatabase { fix_dirs, path, branch } => { // Call the thing - if let Err(err) = generate::policy_database(fix_dirs, path, branch).await { + if let Err(err) = generate::policy_database(fix_dirs, path, &branch).await { error!("{}", err.trace()); std::process::exit(1); } @@ -150,7 +150,7 @@ async fn main() { }, CtlSubcommand::Wizard(subcommand) => match *subcommand { WizardSubcommand::Setup {} => { - if let Err(err) = wizard::setup() { + if let Err(err) = wizard::Wizard::run().await { error!("{}", err.trace()); std::process::exit(1); } diff --git a/brane-ctl/src/wizard.rs b/brane-ctl/src/wizard.rs index 1ad256ae..80f903f3 100644 --- a/brane-ctl/src/wizard.rs +++ b/brane-ctl/src/wizard.rs @@ -6,12 +6,63 @@ // Last edited: // 07 Mar 2024, 09:54:57 // Auto updated? -// Yes +// Yes // // Description: //! Implements a CLI wizard for setting up nodes, making the process //! _even_ easier. // +// +// +//! This module assumes the following default file structure. +//! Where possible the framework will attempt not to enforce this structure, but +//! adhering to this file structure will hopefully allow the framework to infer +//! a lot of locations, which in turn can save a lot of configuration work. +//! +//! If you are setting up only a single node, you can try to use the relevant +//! subdirectory and the framework should be able to work pretty well with this as well +//! +//! ```text +//! configuration +//! ├──central +//! │ ├──config +//! │ │ ├──certs +//! │ │ │ └──hospital1 +//! │ │ │ └──ca.pem +//! │ │ ├──infra.yml +//! │ │ └──proxy.yml +//! │ └──node.yml +//! │ +//! ├──workers +//! │ ├──hospital1 +//! │ │ ├──config +//! │ │ │ ├──certs +//! │ │ │ │ └── +//! │ │ │ ├──backend.yml +//! │ │ │ ├──policy_deliberation_secret.yml +//! │ │ │ ├──policy_expert_secret.yml +//! │ │ │ └──proxy.yml +//! │ │ ├──policies.db +//! │ │ └──node.yml +//! │ │ +//! │ └──research_centre2 +//! │ ├──config +//! │ │ ├──certs +//! │ │ │ └── +//! │ │ ├──backend.yml +//! │ │ ├──policy_deliberation_secret.yml +//! │ │ ├──policy_expert_secret.yml +//! │ │ └──proxy.yml +//! │ ├──policies.db +//! │ └──node.yml +//! │ +//! └──users +//! ├──tim +//! │ ├──jwt_expert.json +//! │ └──jwt_delib.json +//! └──dan +//! └──jwt_expert.json +//! ``` use std::borrow::Cow; use std::collections::HashMap; @@ -19,59 +70,805 @@ use std::error; use std::fmt::{Display, Formatter, Result as FResult}; use std::fs::{self, File}; use std::io::Write as _; +use std::net::IpAddr; use std::path::{Path, PathBuf}; +use std::str::FromStr; + +// I think we might be able to do better than foo, however it is the default in generate as well +const DEFAULT_KEY_ID: &str = "foo"; -use brane_cfg::info::Info; +use brane_cfg::info::{Info, InfoError}; +use brane_cfg::infra::{InfraFile, InfraLocation}; use brane_cfg::node::{self, NodeConfig, NodeKind, NodeSpecificConfig}; use brane_cfg::proxy::{ForwardConfig, ProxyConfig, ProxyProtocol}; -use brane_shr::input::{FileHistory, confirm, input, input_map, input_path, select}; +use brane_shr::input::{FileHistory, confirm, input, input_map, input_option, input_path, select, select_enum}; use console::style; -use dirs::config_dir; +use dirs::data_dir; use enum_debug::EnumDebug as _; -use log::{debug, info}; -use specifications::address::Address; -use validator::{FromStrValidator, MapValidator, PortValidator, RangeValidator}; +use error_trace::trace; +use jsonwebtoken::jwk::KeyAlgorithm; +use log::{debug, info, warn}; +use specifications::address::{Address, Host}; +use specifications::constants::*; +use validator::{FromStrValidator, MapValidator, OptionValidator, PortValidator, RangeValidator}; pub mod validator; +use crate::generate; use crate::spec::InclusiveRange; type PortRangeValidator = RangeValidator; type AddressValidator = FromStrValidator
; +type HostValidator = FromStrValidator; type PortMapValidator = MapValidator; -/***** HELPER MACROS *****/ -/// Generates a FileHistory that points to some branectl-specific directory in the [`config_dir()`]. -macro_rules! hist { - ($name:literal) => {{ - let hist = FileHistory::new(config_dir().unwrap().join("branectl").join("history").join($name)); - debug!("{hist:?}"); - hist - }}; +type LocationId = String; +type LocationIdValidator = FromStrValidator; +type LocationMapValidator = MapValidator; + +type HostnameMapping = HashMap; + +/// Generates a FileHistory that points to some branectl-specific directory in the [`data_dir()`]. +// TODO: We could create something like a history service that will only complain once if we cannot +// create the directory +#[inline(always)] +fn hist(filename: impl AsRef) -> Option { + let Some(data_path) = data_dir() else { + debug!("Could not find path to store history file in."); + return None; + }; + let history_dir = data_path.join("branectl").join("history"); + + let history_path = history_dir.join(filename); + + if !history_path.exists() { + if let Err(err) = fs::create_dir_all(history_dir) { + debug!("{}", trace!(("Could not create directory for history files"), err)); + return None; + } + + return Some(FileHistory::new(history_path)); + } + + Some(FileHistory::from_file_or_new(history_path)) } -/// Writes a few lines that generate a directory, with logging statements. -/// -/// # Arguments -/// - `[$name, $value]`: The name and subsequent value of the variable that contains the given path. -macro_rules! generate_dir { - ($value:ident) => { - if !$value.exists() { - debug!("Generating '{}'...", $value.display()); - if let Err(err) = fs::create_dir(&$value) { - return Err(Error::GenerateDir { path: $value, err }); + +#[inline(always)] +fn generate_dir(path: impl AsRef) -> Result<(), Error> { + let path = path.as_ref(); + if !path.exists() { + debug!("Generating '{}'...", path.display()); + if let Err(err) = fs::create_dir(path) { + return Err(Error::GenerateDir { path: path.to_owned(), err }); + } + } + + Ok(()) +} + +/// Ensures the directory either exists or will ask the user to create it. +/// The functions returns whether the directory exists after the function +#[inline(always)] +fn ensure_dir_with_confirmation(path: impl AsRef, prompt: Option) -> Result { + let path = path.as_ref(); + + if path.exists() { + return Ok(true); + } + + let prompt = prompt.unwrap_or_else(|| format!("Directory '{}' does not exist. Create it?", path.display())); + + if !confirm(prompt, Some(true)).map_err(|err| Error::Input { what: "directory creation confirmation", err })? { + return Ok(false); + } + + debug!("Generating '{}'...", path.display()); + fs::create_dir(path).map_err(|err| Error::GenerateDir { path: path.to_owned(), err })?; + + Ok(true) +} + +#[derive(PartialEq)] +pub enum Wizard { + /// A wizard to set up the node types + Node, + /// A wizard to create the different secrets used in brane + Secrets, + /// A wizard that triggers the wizard for single files + PartialConfiguration, +} + +impl From<&Wizard> for &'static str { + fn from(value: &Wizard) -> Self { + match value { + Wizard::Node => "Node", + Wizard::Secrets => "Secrets", + Wizard::PartialConfiguration => "Partial configuration", + } + } +} + +impl Wizard { + /// The entry point for the wizard. It will start the wizard and start prompting the user to + /// figure out what it wants to create + pub async fn run() -> Result<(), Error> { + info!("Started the branectl wizard"); + + // Do an intro prompt + indoc::printdoc!( + " + + {welcome}{wizard}{brane_version} + + This wizard will guide you through the process of setting up a node interactively. + Simply answer the questions, and the required configuration files will be generated as you go. + + You can abort the wizard a any time by pressing {ctrl_c}. + + ", + ctrl_c = style("Ctrl+C").bold().green(), + welcome = style("Welcome to ").bold(), + wizard = style("Node Setup Wizard").bold().green(), + brane_version = style(format!(" for BRANE v{}", env!("CARGO_PKG_VERSION"))).bold(), + ); + + let mut prompt: Cow = Cow::Borrowed("1. Select the location of the node configuration files"); + let path: PathBuf = loop { + // Query the path + let path: PathBuf = input_path(prompt, Some("./"), hist("output_path.hist")).map_err(|err| Error::Input { what: "config path", err })?; + + // Ask to create it if it does not exist + ensure_dir_with_confirmation(&path, Some(String::from("The configuration directory does not exist yet? Do you want to create it?")))?; + + // Assert it's a directory + if path.is_dir() { + break path; } + prompt = Cow::Owned(format!("Path '{}' does not point to a directory; specify another", path.display())); + }; + debug!("Configuration directory: '{}'", path.display()); + + match select_enum::("What would you like to create/configure?", [Wizard::Node, Wizard::Secrets, Wizard::PartialConfiguration], None) + .map_err(|err| Error::Input { what: "Wizard type", err })? + { + Wizard::Node => { + NodeWizard::run(path).await?; + }, + Wizard::Secrets => { + SecretWizard::run(path)?; + }, + Wizard::PartialConfiguration => { + todo!("In the future it should be possible to separately generate all the configuration files"); + }, } - }; - ($name:ident, $value:expr) => { - let $name: PathBuf = $value; - generate_dir!($name); - }; + Ok(()) + } } +impl Display for Wizard { + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { write!(f, "{}", std::convert::Into::<&str>::into(self)) } +} +enum NodeWizard { + Central, + Worker, +} + +impl NodeWizard { + async fn run(path: impl AsRef) -> Result<(), Error> { + let config_dir = path.as_ref(); + // Select the path where we will go to + println!(); + + // Let us query the user for the type of node + let kind: NodeKind = select("2. Select the type of node to generate", [NodeKind::Central, NodeKind::Worker, NodeKind::Proxy], None) + .map_err(|err| Error::Input { what: "node kind", err })?; + + debug!("Building for node kind '{}'", kind.variant()); + + // Do a small intermittent text, which will be finished by node-specific contexts + indoc::printdoc!( + " + You have selected to create a new {kind} node. + For this node type, the following configuration files have to be generated: + ", + kind = style(kind).bold().green() + ); + + + // The rest is node-dependent + match kind { + NodeKind::Central => CentralNodeWizard::run(config_dir).await?, + NodeKind::Worker => WorkerNodeWizard::run(config_dir).await?, + NodeKind::Proxy => ProxyNodeWizard::run(config_dir)?, + } + + // Done! + Ok(()) + } +} + + +// FIXME: This structure is not entirely correct, we are using a non-constructable enum as a +// namepsace here, we probably want something like modules, but for now we just need to split up +// these 1500 lines into _some_ structure. +enum CentralNodeWizard {} + +impl CentralNodeWizard { + async fn run(configuration_dir: impl AsRef) -> Result<(), Error> { + let configuration_dir = configuration_dir.as_ref(); + + // TODO: Prompt user for input + let hosts = Default::default(); + + let location_id: LocationId = + input("Enter the ", "", None::<&str>, Some(LocationIdValidator::default()), hist("location_id")) + .map_err(|err| Error::Input { what: "Location id", err })?; + + let central_node_dir = configuration_dir.join(&location_id); + let central_node_config_dir = central_node_dir.join("config"); + + generate_dir(¢ral_node_dir)?; + generate_dir(¢ral_node_config_dir)?; + + println!(" - {}", style(central_node_config_dir.join("infra.yml").display()).bold()); + println!(" - {}", style(central_node_config_dir.join("proxy.yml").display()).bold()); + println!(" - {}", style(central_node_config_dir.join("node.yml").display()).bold()); + println!(); + + // === infra.yml === + let infra_path = Self::create_infra_config(configuration_dir, ¢ral_node_config_dir)?; + + // === proxy.yml === + let proxy_path = ProxyNodeWizard::create_proxy_config(¢ral_node_config_dir)?; + + println!("{}", style("=== node.yml ===").bold()); + println!("The default settings for node.yml are listed below:"); + + let use_node_defaults = + confirm("Do you wish to use these defaults?", Some(true)).map_err(|err| Error::Input { what: "default central node", err })?; + + let prx_port = Self::query_service_port("Proxy (PRX)", BRANE_CENTRAL_PRX_PORT, use_node_defaults)?; + let plr_port = Self::query_service_port("Planner (PLR)", BRANE_CENTRAL_PLR_PORT, use_node_defaults)?; + let api_port = Self::query_service_port("Registry (API)", BRANE_CENTRAL_API_PORT, use_node_defaults)?; + let drv_port = Self::query_service_port("Driver (DRV)", BRANE_CENTRAL_DRV_PORT, use_node_defaults)?; + + let prx_name = Self::query_service_name("Proxy (PRX)", BRANE_CENTRAL_PRX_NAME, use_node_defaults)?; + let plr_name = Self::query_service_name("Planner (PLR)", BRANE_CENTRAL_PLR_NAME, use_node_defaults)?; + let api_name = Self::query_service_name("Registry (API)", BRANE_CENTRAL_API_NAME, use_node_defaults)?; + let drv_name = Self::query_service_name("Driver (DRV)", BRANE_CENTRAL_DRV_NAME, use_node_defaults)?; + + type OptionalAddressValidator = OptionValidator; + + let package_path = central_node_dir.join(PACKAGE_PATH); + if !ensure_dir_with_confirmation( + &package_path, + Some(format!("Package directory located at {} does not exist, do you wish to create it?", package_path.display())), + )? { + println!("Did not create a package directory, note that this may impact the functionality of the system"); + } + + let external_proxy: Option
= input_option( + "external proxy", + "Enter the address on which the proxy can be reached from external addresses. Or leave empty to disable", + None::
, + Some(OptionalAddressValidator::default()), + hist("external-proxy"), + ) + .map_err(|err| Error::Input { what: "external proxy", err })?; + let default_hostname = Host::from_str(&gethostname::gethostname().to_string_lossy()) + .inspect_err(|err| warn!("system hostname could not be parsed as a valid hostname: {err:#}")) + .ok(); + + let hostname: Host = + input("hostname", "Enter the hostname for this node", default_hostname, Some(HostValidator::default()), hist("central-hostname")) + .map_err(|err| Error::Input { what: "central hostname", err })?; + + let certs_path = central_node_config_dir.join(CERTIFICATE_PATH); + // TODO: This can be done more elegantly + + if ensure_dir_with_confirmation( + &certs_path, + Some(format!("Certificate directory located at: {} does not exist, do you wish to create it?", certs_path.display())), + )? { + if confirm("Do you wish to create a server certificate?", Some(false)) + .map_err(|err| Error::Input { what: "Create certificate confirmation", err })? + { + let tempdir = tempfile::tempdir().map_err(|err| Error::TempDir { err })?; + generate::certs(true, &certs_path, tempdir.into_path(), crate::spec::GenerateCertsSubcommand::Server { + location_id, + hostname: hostname.to_string(), + }) + .await + .map_err(|err| Error::GenerateError { what: String::from("Server certificate"), err })?; + } + } else { + println!("Could not create certificates without a directory, continuing") + } + + let node_params = crate::spec::GenerateNodeSubcommand::Central { + hostname: hostname.to_string(), + infra: infra_path, + proxy: proxy_path, + certs: certs_path, + packages: package_path, + external_proxy, + api_name, + drv_name, + plr_name, + prx_name, + api_port, + plr_port, + drv_port, + prx_port, + }; + + let node = generate::generate_node(hosts, true, configuration_dir, node_params) + .map_err(|err| Error::GenerateError { what: String::from("central node"), err })?; + + write_config(node, central_node_dir.join("node.yml"), CENTRAL_NODE_CONFIG_URL, Some(NODE_HEADER)) + .map_err(|err| Error::NodeConfigWrite { err: Box::new(err) })?; + + Ok(()) + } + + fn query_service_port(port_name: &str, default_port: u16, use_default: bool) -> Result { + if use_default { + Ok(default_port) + } else { + let port = input( + format!("What port would you like for the {port_name} service?"), + "port", + Some(default_port), + Some(PortValidator {}), + hist("port"), + ) + .map_err(|err| Error::Input { what: "port", err })?; + + Ok(port) + } + } + + fn query_service_name(service_description: &str, default_service_name: &str, use_default: bool) -> Result { + if use_default { + Ok(default_service_name.to_string()) + } else { + let x = input( + format!("What port would you like for the {service_description} service?"), + "port", + Some(default_service_name), + // FIXME: Wrong validator + Some(PortValidator {}), + hist("service_name"), + ) + .map_err(|err| Error::Input { what: "port", err })?; + + Ok(x) + } + } + + async fn create_central_node_prerequisites( + configuration_dir: impl AsRef, + central_node_dir: impl AsRef, + ) -> Result<(PathBuf, PathBuf), Error> { + let config_dir = central_node_dir.as_ref(); + + println!(" - {}", style(config_dir.join("infra.yml").display()).bold()); + println!(" - {}", style(config_dir.join("proxy.yml").display()).bold()); + println!(" - {}", style(config_dir.join("node.yml").display()).bold()); + println!(); + + let proxy_config_path = ProxyNodeWizard::create_proxy_config(¢ral_node_dir)?; + let infra_config_path = Self::create_infra_config(&configuration_dir, ¢ral_node_dir)?; + + println!("{}", style("=== node.yml ===").bold()); + + Ok((infra_config_path, proxy_config_path)) + } + + fn create_infra_config(configuration_dir: impl AsRef, central_node_dir: impl AsRef) -> Result { + let _path = configuration_dir.as_ref(); + let central_node_dir = central_node_dir.as_ref(); + + println!("{}", style("=== infra.yml ===").bold()); + + generate_dir(central_node_dir)?; + + let infra_file = query_infra_config()?; + + indoc::printdoc!( + " + One can set the ports for all services on the worker in case these are different from the defaults. + This however is not yet supported in the generator. If you need this behaviour. It is recommended you use `branectl generate` instead." + ); + let infra_path = central_node_dir.join("infra.yml"); + + write_config(infra_file, &infra_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/infra.html", None) + .map_err(|err| Error::InfraConfigWrite { err: Box::new(err) })?; + + Ok(infra_path) + } +} + +enum WorkerNodeWizard {} + +impl WorkerNodeWizard { + async fn run(config_dir: impl AsRef) -> Result<(), Error> { + let config_dir = config_dir.as_ref(); + + let location_id: LocationId = input( + "worker location id", + "What is the location id of this worker?", + None::<&str>, + Some(LocationIdValidator::default()), + hist("worker-location-id"), + ) + .map_err(|err| Error::Input { what: "worker location id", err })?; + + // TODO: Sanitize name for path use + let worker_path = config_dir.join(&location_id); + + println!(" - {}", style(config_dir.join("backend.yml").display()).bold()); + println!(" - {}", style(config_dir.join("proxy.yml").display()).bold()); + println!(" - {}", style(config_dir.join("node.yml").display()).bold()); + println!(); + + println!("And lastly:"); + println!(" - {}", style("A 802.1X certificate").bold()); + println!(" - {}", style("policies.db").bold()); + println!(); + + println!("{}", style("=== backend.yml ===").bold()); + println!("{}", style("=== proxy.yml ===").bold()); + println!("{}", style("=== node.yml ===").bold()); + + Self::create_policy_database(&worker_path).await?; + Self::create_certificate(&worker_path, &location_id).await?; + + Ok(()) + } + + async fn create_policy_database(worker_path: impl AsRef) -> Result<(), Error> { + let worker_path = worker_path.as_ref(); + + println!("{}", style("=== policies.db ===").bold()); + if confirm("Would you like to generate a new policy database?", Some(true)) + .map_err(|err| Error::Input { what: "new policy database", err })? + { + let policy_database_path = worker_path.join("policies.db"); + + if !policy_database_path.exists() + || confirm( + indoc::formatdoc!( + " + There already exists a policy database on this location, would you like to override it and create a new one? + + {warning} + ", + warning = style("Warning: This will erase all the data in the old database.").bold().red() + ), + Some(false), + ) + .map_err(|err| Error::Input { what: "remove old policy database", err })? + { + generate::policy_database(true, policy_database_path, "main") + .await + .map_err(|err| Error::GenerateError { what: String::from("policy database"), err })?; + } + } + + Ok(()) + } + + async fn create_certificate(worker_path: impl AsRef, location_id: &LocationId) -> Result { + let worker_path = worker_path.as_ref(); + let certificate_path = worker_path.join("certs"); + + println!("{}", style("=== 801.1X certificate ===").bold()); + + if confirm("Do you want to create a 801.1X certificate?", Some(true)) + .map_err(|err| Error::Input { what: "Confirmation creation 801.1X certificate", err })? + { + let default_hostname = Host::from_str(&gethostname::gethostname().to_string_lossy()) + .inspect_err(|err| warn!("system hostname could not be parsed as a valid hostname: {err:#}")) + .ok(); + + let hostname: Host = input( + "What hostname will be used in this certificate?", + "hostname", + default_hostname, + None::, + hist("ca-hostname"), + ) + .map_err(|err| Error::Input { what: "hostname", err })?; + + let tmp = tempfile::tempdir() + // FIXME: This is not the right error, but bigger fish to fry first + .map_err(|err| Error::ConfigCreate { path: "temporary path".into(), err })?; + + generate::certs(true, &certificate_path, tmp.into_path(), crate::spec::GenerateCertsSubcommand::Client { + location_id: location_id.to_string(), + hostname: hostname.to_string(), + ca_cert: "ca.pem".into(), + ca_key: "ca-key.pem".into(), + }) + .await + .map_err(|err| Error::GenerateError { what: String::from("certificate"), err })?; + + if confirm("Do you want to install the certificate on another node?", None) + .map_err(|err| Error::Input { what: "confirmation installation certificate", err })? + { + // TODO: Guess default path + let destination_path = + input_path("Select a node (node.yml) where you want to install this certificate.", None::<&str>, hist("certificate-dest.hist")) + .map_err(|err| Error::Input { what: "certificate destination", err })?; + + let destination_node = + NodeConfig::from_path(destination_path).map_err(|err| Error::NodeSerialize { what: "certificate destination node", err })?; + + Self::install_certificate(&certificate_path, location_id, destination_node) + .map_err(|err| Error::InstallCertificate { path: certificate_path.clone(), err })?; + } + } + + Ok(certificate_path) + } + + fn install_certificate( + certificate_path: impl AsRef, + source_location_id: &LocationId, + destination_node: NodeConfig, + ) -> Result { + let certificate_path = certificate_path.as_ref(); + let destination = match destination_node.node { + NodeSpecificConfig::Central(config) => config.paths.certs, + NodeSpecificConfig::Worker(config) => config.paths.certs, + NodeSpecificConfig::Proxy(config) => config.paths.certs, + }; + + let destination_path = destination.join(source_location_id); + + std::fs::copy(certificate_path, &destination_path)?; + + Ok(destination_path) + } + + fn create_policy_token(dir: impl AsRef, secret_path: impl AsRef) -> Result<(), Error> { + let dir = dir.as_ref(); + let secret_path = secret_path.as_ref(); + + println!("{}", style("=== policy_token.json ===").bold()); + if confirm("Would you like to generate a `policy_token.json`", Some(true)) + .map_err(|err| Error::Input { what: "confirmation policy token", err })? + { + let initiator = input( + "initiator", + "Enter the initiator. The name of the person performing the request.", + None::<&str>, + None::, + hist("policy-token-initiator.hist"), + ) + .map_err(|err| Error::Input { what: "policy token initiator", err })?; + + let system = input( + "system", + "Enter the system. The name or identifier of the node or other entity through which the request is performed, to embed in the token.", + None::<&str>, + None::, + hist("policy-token-initiator.hist"), + ) + .map_err(|err| Error::Input { what: "policy token initiator", err })?; + + // TODO: Add validator + let exp: String = input( + "expiration duration", + "Enter the duration after which the token will expire. E.g. 1y", + Some(String::from("1 year")), + Some(|inp: &String| { + humantime::parse_duration(inp).map(|_| ()) + }), + hist("expiration-duration.hist"), + ) + .map_err(|err| Error::Input { what: "expiration duration", err })?; + + // Is validated so should never happen anyway + let exp = humantime::parse_duration(&exp).expect("policy token expiration duration"); + + // TODO: Handle error + let _ = generate::policy_token(true, dir.join("policy_token.json"), secret_path.to_path_buf(), initiator, system, exp); + todo!("Generating secret is not yet supported via the wizard") + } + + Ok(()) + } + + fn create_expert_secret(worker_path: impl AsRef) -> Result<(), Error> { + let worker_path = worker_path.as_ref(); + + println!("{}", style("=== policy_expert_secret.json ===").bold()); + if confirm("Would you like to generate a `policy_expert_secret.json`", Some(true)) + .map_err(|err| Error::Input { what: "confirmation policy_expert_secret", err })? + { + // TODO: Generate secret + // TODO: Make key id optionally overridable + generate::policy_secret(true, worker_path.join("policy_expert_secret.json"), DEFAULT_KEY_ID.to_owned(), KeyAlgorithm::HS256) + .map_err(|err| Error::GenerateError { what: String::from("policy expert secret"), err })?; + todo!("Generating secret is not yet supported via the wizard") + } + + Ok(()) + } + + fn create_deliberation_secret(worker_path: impl AsRef) -> Result<(), Error> { + let worker_path = worker_path.as_ref(); + + println!("{}", style("=== policy_deliberation_secret.json ===").bold()); + if confirm("Would you like to generate a `policy_deliberation_secret.json`", Some(true)) + .map_err(|err| Error::Input { what: "confirmation policy_deliberation_secret", err })? + { + // TODO: Generate secret + // generate::policy_secret(fix_dirs, path, key_id, key_alg); + generate::policy_secret(true, worker_path.join("policy_deliberation_secret.json"), DEFAULT_KEY_ID.to_owned(), KeyAlgorithm::HS256) + .map_err(|err| Error::GenerateError { what: String::from("deliberation secret"), err })?; + todo!("Generating secret is not yet supported via the wizard") + } + + Ok(()) + } +} + +enum ProxyNodeWizard {} + +impl ProxyNodeWizard { + fn run(config_dir: impl AsRef) -> Result<(), Error> { + let config_dir = config_dir.as_ref(); + + println!(" - {}", style(config_dir.join("proxy.yml").display()).bold()); + println!(); + + // Note: we don't check if the user wants a custom config, since they very likely want it if they are setting up a proxy node + // For the proxy, we only need to read the proxy config + println!("{}", style("=== proxy.yml ===").bold()); + + let cfg = query_proxy_config().map_err(|err| Error::ProxyConfigQuery { err: Box::new(err) })?; + + let proxy_path = config_dir.join("proxy.yml"); + + write_config(cfg, proxy_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/proxy.html", Some(PROXY_HEADER)) + .map_err(|err| Error::ProxyConfigWrite { err: Box::new(err) })?; + + // Now we generate the node.yml file + println!("{}", style("=== node.yml ===")); + let node = query_proxy_node_config().map_err(|err| Error::NodeConfigQuery { err: Box::new(err) })?; + + let node_path = config_dir.join("node.yml"); + + write_config(node, node_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/node.html", None) + .map_err(|err| Error::NodeConfigWrite { err: Box::new(err) })?; + + Ok(()) + } + + // FIXME: This method overlaps to much with the method above + fn create_proxy_config(central_node_dir: impl AsRef) -> Result { + let central_node_dir = central_node_dir.as_ref(); + + println!("{}", style("=== proxy.yml ===").bold()); + let proxy_config_source = select_enum::( + "How do you prefer to configure proxy.yml?", + // TODO: Maybe use strum or something to get 'm all + [ProxyConfigSource::Default, ProxyConfigSource::ExistingFile, ProxyConfigSource::Prompt], + None, + ) + .map_err(|err| Error::Input { what: "config source", err })?; + + let proxy = match proxy_config_source { + ProxyConfigSource::Default => ProxyConfig::default(), + ProxyConfigSource::ExistingFile => { + let path = input_path("Select the existing proxy.yml on your system", None::, hist("proxy-path.hist")) + .map_err(|err| Error::Input { what: "proxy.yml path", err })?; + println!("Using proxy.yml from: `{}`", path.display()); + println!("Note: that this will make a copy of this file. So changing it afterwards will have no effect."); + ProxyConfig::from_path(path).map_err(|err| Error::ProxyConfigRead { err })? + }, + ProxyConfigSource::Prompt => query_proxy_config()?, + }; + + let proxy_path = central_node_dir.join("proxy.yml"); + + write_config(proxy, &proxy_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/proxy.html", None) + .map_err(|err| Error::ProxyConfigWrite { err: Box::new(err) })?; + + Ok(proxy_path) + } +} + +pub enum SecretWizard { + ExpertToken, + DeliberationToken, +} + +impl SecretWizard { + pub fn run(path: impl AsRef) -> Result<(), Error> { + let _path = path.as_ref(); + + // TODO: Make general (worker) node selector + let worker_path = input_path("Select the `node.yml` for the worker node you like to interact with.", None::<&str>, hist("worker-node")) + .map_err(|err| Error::Input { what: "worker node configuration", err })?; + + use SecretWizard::*; + // TODO: Allow the selection of multiple + match select("What secret would you like to create?", [ExpertToken, DeliberationToken], None) + .map_err(|err| Error::Input { what: "node kind", err })? + { + DeliberationToken => Self::create_deliberation_token(worker_path)?, + ExpertToken => Self::create_expert_token(worker_path)?, + } + + Ok(()) + } + + pub fn create_expert_token(worker_path: impl AsRef) -> Result<(), Error> { + let username: String = + input("username", "What username would you like to use?", std::env::var("USER").ok(), None::, hist("username")) + .map_err(|err| Error::Input { what: "policy token username", err })?; + let system: String = + input("username", "What system is this token for?", std::env::var("USER").ok(), None::, hist("system")) + .map_err(|err| Error::Input { what: "policy token system", err })?; + + todo!(); + } + + pub fn create_deliberation_token(worker_path: impl AsRef) -> Result<(), Error> { + let worker_path = worker_path.as_ref(); + + let username: String = + input("username", "What username would you like to use?", std::env::var("USER").ok(), None::, hist("username")) + .map_err(|err| Error::Input { what: "policy token username", err })?; + + // TODO: Clarify, I suspect the system in the case of Brane would be the worker. + let system: String = input("username", "What system is this token for?", None::<&str>, None::, hist("system")) + .map_err(|err| Error::Input { what: "policy token system", err })?; + + // TODO: How do we know what worker this token is for in case of multiple workers + let default_secret_path = worker_path.join("policy_deliberation_secret.yml"); + let default_secret_path = if default_secret_path.exists() { Some(default_secret_path) } else { None }; + + let secret_path: PathBuf = input_path("Using what secret do you want to use for this token?", default_secret_path, hist("system")) + .map_err(|err| Error::Input { what: "policy token system", err })?; + + // TODO: Use some "smart" logic to most likely location of the secret. + let destination_path: PathBuf = input_path("Where would you like to store this token?", None::<&Path>, hist("system")) + .map_err(|err| Error::Input { what: "policy token system", err })?; + + // TODO: We default to 1 year now, but we have to make this configurable + + generate::policy_token(true, destination_path.clone(), secret_path, username, system, std::time::Duration::from_secs(86400 * 365)) + .map_err(|err| Error::GenerateError { what: String::from("Deliberation secret"), err })?; + + println!("Succesfully generated a deliberation token at: {path}", path = destination_path.display()); + + Ok(()) + } +} + +impl From<&SecretWizard> for &'static str { + fn from(value: &SecretWizard) -> Self { + match value { + // SecretWizard::ExpertSecret => "Policy expert secret", + // SecretWizard::DeliberationSecret => "Policy deliberation secret", + SecretWizard::ExpertToken => "Policy expert token", + SecretWizard::DeliberationToken => "Policy deliberation token", + } + } +} + +impl Display for SecretWizard { + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { write!(f, "{}", std::convert::Into::<&str>::into(self)) } +} /***** ERRORS *****/ @@ -79,27 +876,80 @@ macro_rules! generate_dir { #[derive(Debug)] pub enum Error { /// Failed to query the user for the node config file. - NodeConfigQuery { err: Box }, + NodeConfigQuery { + err: Box, + }, /// Failed to write the node config file. - NodeConfigWrite { err: Box }, + NodeConfigWrite { + err: Box, + }, /// Failed to query the user for the proxy config file. - ProxyConfigQuery { err: Box }, + ProxyConfigQuery { + err: Box, + }, + /// Failed to write the proxy config file. + ProxyConfigWrite { + err: Box, + }, /// Failed to write the proxy config file. - ProxyConfigWrite { err: Box }, + ProxyConfigRead { + err: brane_cfg::info::YamlError, + }, - /// Failed to create a new file. - ConfigCreate { path: PathBuf, err: std::io::Error }, + /// Failed to create a new file!(). + ConfigCreate { + path: PathBuf, + err: std::io::Error, + }, /// Failed to generate a configuration file. - ConfigSerialize { path: PathBuf, err: brane_cfg::info::YamlError }, + ConfigSerialize { + path: PathBuf, + err: brane_cfg::info::YamlError, + }, /// Failed to write to the config file. - ConfigWrite { path: PathBuf, err: std::io::Error }, + ConfigWrite { + path: PathBuf, + err: std::io::Error, + }, + /// Failed to generate a directory. - GenerateDir { path: PathBuf, err: std::io::Error }, + GenerateDir { + path: PathBuf, + err: std::io::Error, + }, + /// Failed the query the user for input. - /// /// The `what` should fill in: `Failed to query the user for ...` - Input { what: &'static str, err: brane_shr::input::Error }, + Input { + what: &'static str, + err: brane_shr::input::Error, + }, + InfraConfigWrite { + err: Box, + }, + PathCanonicalize { + what: &'static str, + path: PathBuf, + err: std::io::Error, + }, + GenerateError { + what: String, + err: generate::Error, + }, + + TempDir { + err: std::io::Error, + }, + NodeSerialize { + what: &'static str, + err: InfoError, + }, + InstallCertificate { + path: PathBuf, + err: std::io::Error, + }, } + impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> FResult { use Error::*; @@ -108,12 +958,21 @@ impl Display for Error { NodeConfigWrite { .. } => write!(f, "Failed to write node config file"), ProxyConfigQuery { .. } => write!(f, "Failed to query proxy service configuration"), ProxyConfigWrite { .. } => write!(f, "Failed to write proxy service config file"), + // TODO: Maybe we should elaborate on this error. I realise that it is easy to + // misinterpret this error as writing the error. + ProxyConfigRead { .. } => write!(f, "Failed to read proxy service config file"), + InfraConfigWrite { .. } => write!(f, "Failed to write infra config file"), ConfigCreate { path, .. } => write!(f, "Failed to create config file '{}'", path.display()), ConfigSerialize { path, .. } => write!(f, "Failed to serialize config to '{}'", path.display()), ConfigWrite { path, .. } => write!(f, "Failed to write to config file '{}'", path.display()), GenerateDir { path, .. } => write!(f, "Failed to generate directory '{}'", path.display()), Input { what, .. } => write!(f, "Failed to query the user for {what}"), + PathCanonicalize { what, path, .. } => write!(f, "Failed to canonicalize the {what} path: {}", path.display()), + GenerateError { .. } => todo!(), + TempDir { .. } => write!(f, "Could not generate temporary directory"), + NodeSerialize { what, .. } => write!(f, "Could not serialize {}", what), + InstallCertificate { path, .. } => write!(f, "Could not install certificate to {}", path.display()), } } } @@ -123,23 +982,58 @@ impl error::Error for Error { match self { NodeConfigQuery { err } => Some(err), NodeConfigWrite { err } => Some(err), + ProxyConfigQuery { err } => Some(err), ProxyConfigWrite { err } => Some(err), + ProxyConfigRead { err } => Some(err), + + InfraConfigWrite { err } => Some(err), ConfigCreate { err, .. } => Some(err), ConfigSerialize { err, .. } => Some(err), ConfigWrite { err, .. } => Some(err), GenerateDir { err, .. } => Some(err), Input { err, .. } => Some(err), + PathCanonicalize { err, .. } => Some(err), + GenerateError { .. } => todo!(), + TempDir { err } => Some(err), + NodeSerialize { err, .. } => Some(err), + InstallCertificate { err, .. } => Some(err), } } } +/***** HELPER FUNCTIONS *****/ +fn create_header(filename: impl AsRef) -> String { + let filename = filename.as_ref(); + let mut header = String::with_capacity(filename.len()); + let mut last_char_lowercase = false; + let mut chars = filename.chars().peekable(); + for c in chars.by_ref().take_while(|&c| c != '.') { + if c == ' ' || c == '-' || c == '_' { + // Write it as a space + header.push(' '); + } else if last_char_lowercase && c.is_ascii_uppercase() { + // Write is with a space, since we assume it's a word boundary in camelCase + header.push(' '); + header.push(c); + } else if c.is_ascii_lowercase() { + // Capitalize it + header.push(c.to_ascii_uppercase()); + } + // Update whether we saw a lowercase last step + last_char_lowercase = c.is_ascii_lowercase(); + } + if chars.peek().is_some() { + header.push('.'); + header.extend(chars); + } -/***** HELPER FUNCTIONS *****/ + header +} /// Writes a given [`Config`] to disk. /// /// This wraps the default [`Config::to_path()`] function to also include a nice header. @@ -154,7 +1048,7 @@ impl error::Error for Error { /// /// # Panics /// This function may panic if the given path has no filename. -fn write_config(config: C, path: impl AsRef, url: impl AsRef) -> Result<(), Error> +fn write_config(config: C, path: impl AsRef, url: impl AsRef, header: Option<&str>) -> Result<(), Error> where C: Info, { @@ -165,84 +1059,42 @@ where // Deduce the filename let filename: Cow = match path.file_name() { Some(filename) => filename.to_string_lossy(), - None => { - panic!("No filename found in '{}'", path.display()); - }, + // FIXME: Panic seems excessive + None => panic!("No filename found in '{}'", path.display()), }; - // Convert the filename to nice header - let mut header_name: String = String::with_capacity(filename.len()); - let mut saw_lowercase: bool = false; - let mut ext: bool = false; - for c in filename.chars() { - if !ext && c == '.' { - // Move to extension mode - header_name.push('.'); - ext = true; - } else if !ext && (c == ' ' || c == '-' || c == '_') { - // Write it as a space - header_name.push(' '); - } else if !ext && saw_lowercase && c.is_ascii_uppercase() { - // Write is with a space, since we assume it's a word boundary in camelCase - header_name.push(' '); - header_name.push(c); - } else if !ext && c.is_ascii_lowercase() { - // Capitalize it - header_name.push((c as u8 - b'a' + b'A') as char); - } else { - // The rest is pushed as-is - header_name.push(c); - } - - // Update whether we saw a lowercase last step - saw_lowercase = c.is_ascii_lowercase(); - } - // Create a file, now - let mut handle: File = match File::create(path) { - Ok(handle) => handle, - Err(err) => { - return Err(Error::ConfigCreate { path: path.into(), err }); - }, - }; + let mut handle = File::create(path).map_err(|err| Error::ConfigCreate { path: path.into(), err })?; // Write the header to a string - if let Err(err) = writeln!(handle, "# {header_name}") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# by branectl") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# ") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# This file has been generated using the `branectl wizard` subcommand. You can") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# manually change this file after generation; it is just a normal YAML file.") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# Documentation for how to do so can be found here:") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# {url}") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle, "# ") { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; - if let Err(err) = writeln!(handle) { - return Err(Error::ConfigWrite { path: path.into(), err }); - }; + let wizard_header = indoc::formatdoc!( + " + # {name} + # generated by branectl v{brane_version} + # + # This file has been generated using the `branectl wizard` subcommand. You can, + # manually change this file after generation; it is just a normal YAML file., + # Documentation for how to do so can be found here:, + # {url} + ", + name = create_header(filename), + url = url, + brane_version = env!("CARGO_PKG_VERSION"), + ); - // Write the remainder of the file - if let Err(err) = config.to_writer(handle, true) { - return Err(Error::ConfigSerialize { path: path.into(), err }); + writeln!(handle, "{}", wizard_header).map_err(|err| Error::ConfigWrite { path: path.into(), err })?; + + if let Some(file_header) = header { + writeln!(handle, "{}", file_header).map_err(|err| Error::ConfigWrite { path: path.into(), err })?; } + + // Write the remainder of the file + config.to_writer(handle, true).map_err(|err| Error::ConfigSerialize { path: path.into(), err })?; + Ok(()) } -/***** QUERY FUNCTIONS *****/ +/***** PROXY FUNCTIONS *****/ /// Queries the user for the proxy services configuration. /// /// # Returns @@ -252,23 +1104,20 @@ where /// This function may error if we failed to query the user. pub fn query_proxy_config() -> Result { // Query the user for the range - let range: InclusiveRange = match input( + let range: InclusiveRange = input( "port range", "P1. Enter the range of ports allocated for outgoing connections", Some(InclusiveRange::new(4200, 4299)), Some(PortRangeValidator::default()), - Some(hist!("prx-outgoing_range.hist")), - ) { - Ok(range) => range, - Err(err) => { - return Err(Error::Input { what: "outgoing range", err }); - }, - }; + hist("prx-outgoing_range.hist"), + ) + .map_err(|err| Error::Input { what: "outgoing range", err })?; + debug!("Outgoing range: [{}, {}]", range.0.start(), range.0.end()); println!(); // Read the map of incoming ports - let incoming: HashMap = match input_map( + let incoming: HashMap = input_map( "port", "address", "P2.1. Enter an incoming port map as '::' (or leave empty to specify none)", @@ -276,46 +1125,31 @@ pub fn query_proxy_config() -> Result { ":", // None::, Some(PortMapValidator { allow_empty: true, ..Default::default() }), - Some(hist!("prx-incoming.hist")), - ) { - Ok(incoming) => incoming, - Err(err) => { - return Err(Error::Input { what: "outgoing range", err }); - }, - }; + hist("prx-incoming.hist"), + ) + .map_err(|err| Error::Input { what: "outgoing range", err })?; + debug!("Incoming ports map:\n{:#?}", incoming); println!(); // Finally, read any proxy - let to_proxy_or_not_to_proxy: bool = match confirm("P3. Do you want to route outgoing traffic through a SOCKS proxy?", Some(false)) { - Ok(yesno) => yesno, - Err(err) => { - return Err(Error::Input { what: "proxy confirmation", err }); - }, - }; + let to_proxy_or_not_to_proxy: bool = confirm("P3. Do you want to route outgoing traffic through a SOCKS proxy?", Some(false)) + .map_err(|err| Error::Input { what: "proxy confirmation", err })?; let forward: Option = if to_proxy_or_not_to_proxy { // Query the address - let address: Address = match input( + let address: Address = input( "address", "P3a. Enter the target address (including port) to route the traffic to", None::
, Some(AddressValidator::default()), - Some(hist!("prx-forward-address.hist")), - ) { - Ok(address) => address, - Err(err) => { - return Err(Error::Input { what: "forwarding address", err }); - }, - }; + hist("prx-forward-address.hist"), + ) + .map_err(|err| Error::Input { what: "forwarding address", err })?; // Query the protocol let protocol: ProxyProtocol = - match select("P3b. Enter the protocol to use to route traffic", vec![ProxyProtocol::Socks5, ProxyProtocol::Socks6], Some(0)) { - Ok(prot) => prot, - Err(err) => { - return Err(Error::Input { what: "forwarding protocol", err }); - }, - }; + select("P3b. Enter the protocol to use to route traffic", vec![ProxyProtocol::Socks5, ProxyProtocol::Socks6], Some(0)) + .map_err(|err| Error::Input { what: "forwarding protocol", err })?; // Construct the config Some(ForwardConfig { address, protocol }) @@ -338,149 +1172,119 @@ pub fn query_proxy_config() -> Result { /// This function may error if we failed to query the user. pub fn query_proxy_node_config() -> Result { // Construct the ProxyConfig to return it + let hostnames: HostnameMapping = input_map( + "", + "
", + "P2.1. Enter an worker mapping as: ':' (or leave empty to specify none)", + "P2.%I. Enter an additional worker mapping as ':' (or leave empty to finish)", + ":", + // None::, + Some(LocationMapValidator { allow_empty: true, ..Default::default() }), + hist("location-map.hist"), + ) + .map_err(|err| Error::Input { what: "outgoing range", err })?; + + let namespace: String = input( + "docker compose namespace", + "Enter the docker compose namespace (project name) for this node", + None::<&str>, + Some(LocationMapValidator { allow_empty: true, ..Default::default() }), + hist("namespace.hist"), + ) + .map_err(|err| Error::Input { what: "outgoing range", err })?; + + // FIXME: Bind address and port can be combined in an more clever way + let bind_address: IpAddr = input( + "bind address", + "What address should the proxy bind to?", + Some(IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))), + None::, + hist("bind_address.hist"), + ) + .map_err(|err| Error::Input { what: "bind address", err })?; + + let bind_port: u16 = input( + "docker compose namespace", + "Enter the docker compose namespace (project name) for this node", + None::, + None::, + hist("bind_port.hist"), + ) + .map_err(|err| Error::Input { what: "bind port", err })?; + let hostnames = Default::default(); + let bind_address = std::net::SocketAddr::from((bind_address, bind_port)); + + // FIXME: Bind address and port can be combined in an more clever way + let external_address: Host = + input("external address", "What address should the proxy bind to?", None::, None::, hist("bind_address.hist")) + .map_err(|err| Error::Input { what: "bind address", err })?; + Ok(NodeConfig { - hostnames: HashMap::new(), - namespace: String::new(), - node: NodeSpecificConfig::Proxy(node::ProxyConfig { + hostnames, + namespace, + node: NodeSpecificConfig::Proxy(node::ProxyConfig { paths: node::ProxyPaths { certs: "".into(), proxy: "".into() }, services: node::ProxyServices { prx: node::PublicService { name: "brane-prx".into(), - address: Address::Hostname("test.com".into(), 42), - bind: std::net::SocketAddr::V4(std::net::SocketAddrV4::new(std::net::Ipv4Addr::new(0, 0, 0, 0), 0)), - external_address: Address::Hostname("test.com".into(), 42), + address: Address::Hostname("brane-prx".into(), bind_port), + bind: bind_address, + external_address: (external_address, bind_port).into(), }, }, }), }) } +#[derive(PartialEq)] +enum ProxyConfigSource { + Default, + ExistingFile, + Prompt, +} - - - -/***** LIBRARY *****/ -/// Main handler for the `branectl wizard setup` (or `branectl wizard node`) subcommand. -/// -/// # Arguments -/// -/// # Errors -/// This function may error if any of the wizard steps fail. -pub fn setup() -> Result<(), Error> { - info!("Running wizard to setup a new node..."); - - // Let us setup the history structure - generate_dir!(_path, config_dir().unwrap().join("branectl")); - generate_dir!(_path, config_dir().unwrap().join("branectl").join("history")); - - // Do an intro prompt - println!(); - println!( - "{}{}{}", - style("Welcome to ").bold(), - style("Node Setup Wizard").bold().green(), - style(format!(" for BRANE v{}", env!("CARGO_PKG_VERSION"))).bold() - ); - println!(); - println!("This wizard will guide you through the process of setting up a node interactively."); - println!("Simply answer the questions, and the required configuration files will be generated as you go."); - println!(); - println!("You can abort the wizard at any time by pressing {}.", style("Ctrl+C").bold().green()); - println!(); - - // Select the path where we will go to - let mut prompt: Cow = Cow::Borrowed("1. Select the location of the node configuration files"); - let path: PathBuf = loop { - // Query the path - let path: PathBuf = match input_path(prompt, Some("./"), Some(hist!("output_path.hist"))) { - Ok(path) => path, - Err(err) => { - return Err(Error::Input { what: "config path", err }); - }, - }; - - // Ask to create it if it does not exist - if !path.exists() { - // Do the question - let ok: bool = match confirm("Directory '{}' does not exist. Create it?", Some(true)) { - Ok(ok) => ok, - Err(err) => { - return Err(Error::Input { what: "directory creation confirmation", err }); - }, - }; - - // Create it, lest continue and try again - if ok { - generate_dir!(path); - } - } - - // Assert it's a directory - if path.is_dir() { - break path; +impl From<&ProxyConfigSource> for &'static str { + fn from(value: &ProxyConfigSource) -> Self { + match value { + ProxyConfigSource::Default => "Use the default configuration (often recommended)", + ProxyConfigSource::ExistingFile => "Use existing proxy.yml on your filesystem", + ProxyConfigSource::Prompt => "Configure it right now.", } - prompt = Cow::Owned(format!("Path '{}' does not point to a directory; specify another", path.display())); - }; - debug!("Configuration directory: '{}'", path.display()); - println!(); - - // Generate the configuration directories already - generate_dir!(config_dir, path.join("config")); - generate_dir!(certs_dir, config_dir.join("certs")); - - // Let us query the user for the type of node - let kind: NodeKind = match select("2. Select the type of node to generate", [NodeKind::Central, NodeKind::Worker, NodeKind::Proxy], None) { - Ok(kind) => kind, - Err(err) => { - return Err(Error::Input { what: "node kind", err }); - }, - }; - debug!("Building for node kind '{}'", kind.variant()); - println!(); - - // Do a small intermittent text, which will be finished by node-specific contexts - println!("You have selected to create a new {} node.", style(kind).bold().green()); - println!("For this node type, the following configuration files have to be generated:"); - - // The rest is node-dependent - match kind { - NodeKind::Central => {}, + } +} - NodeKind::Worker => {}, +impl Display for ProxyConfigSource { + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { write!(f, "{}", Into::<&str>::into(self)) } +} - NodeKind::Proxy => { - println!(" - {}", style(config_dir.join("proxy.yml").display()).bold()); - println!(); - // Note: we don't check if the user wants a custom config, since they very likely want it if they are setting up a proxy node - // For the proxy, we only need to read the proxy config - println!("=== proxy.yml==="); - let cfg: ProxyConfig = match query_proxy_config() { - Ok(cfg) => cfg, - Err(err) => { - return Err(Error::ProxyConfigQuery { err: Box::new(err) }); - }, - }; - let proxy_path: PathBuf = config_dir.join("proxy.yml"); - if let Err(err) = write_config(cfg, proxy_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/proxy.html") { - return Err(Error::ProxyConfigWrite { err: Box::new(err) }); - } +/***** INFRA FUNCTIONS *****/ +/// TODO: Documentation +pub fn query_infra_config() -> Result { + // Read the map of incoming ports + let _worker_mapping: HashMap = input_map( + "", + "
", + "P2.1. Enter an worker mapping as: ':' (or leave empty to specify none)", + "P2.%I. Enter an additional worker mapping as ':' (or leave empty to finish)", + ":", + // None::, + Some(LocationMapValidator { allow_empty: true, ..Default::default() }), + hist("location-map.hist"), + ) + .map_err(|err| Error::Input { what: "outgoing range", err })?; - // Now we generate the node.yml file - println!("=== node.yml ==="); - let node: NodeConfig = match query_proxy_node_config() { - Ok(node) => node, - Err(err) => { - return Err(Error::NodeConfigQuery { err: Box::new(err) }); - }, - }; - let node_path: PathBuf = path.join("node.yml"); - if let Err(err) = write_config(node, node_path, "https://wiki.enablingpersonalizedinterventions.nl/user-guide/config/admins/node.html") { - return Err(Error::NodeConfigWrite { err: Box::new(err) }); - } - }, - } + let infra_locations = _worker_mapping + .into_iter() + .map(|(location_id, host)| { + (location_id.clone(), InfraLocation { + // TODO: Prompt for the human readable name + name: location_id, + registry: Address::hostname(format!("https://{}", host), BRANE_WORKER_JOB_PORT), + delegate: Address::hostname(format!("grpc://{}", host), BRANE_WORKER_REG_PORT), + }) + }) + .collect::>(); - // Done! - Ok(()) + Ok(InfraFile::new(infra_locations)) } diff --git a/brane-ctl/src/wizard/validator.rs b/brane-ctl/src/wizard/validator.rs index 45426540..2f692cc6 100644 --- a/brane-ctl/src/wizard/validator.rs +++ b/brane-ctl/src/wizard/validator.rs @@ -164,3 +164,42 @@ where } } } + +/// Implenents a validator for optional fields. This either inputs that are valid for the wrapped +/// validator or empty string (and whitespace characters) +// Unfortunately, due to the orphan rule we cannot simply implement this for Option, so we have +// to wrap it. +pub struct OptionValidator +where + T: std::str::FromStr, + V: InputValidator, +{ + pub validator: V, + _fd: std::marker::PhantomData, +} + +impl InputValidator for OptionValidator +where + V: InputValidator, + T: ToString, + T: std::str::FromStr, +{ + type Err = V::Err; + + fn validate(&mut self, input: &T) -> Result<(), Self::Err> { + if input.to_string().trim().is_empty() { + return Ok(()); + } + + return self.validator.validate(input); + } +} + +impl Default for OptionValidator +where + T: std::str::FromStr, + V: InputValidator, + V: Default, +{ + fn default() -> Self { Self { validator: Default::default(), _fd: std::marker::PhantomData } } +} diff --git a/brane-shr/src/input.rs b/brane-shr/src/input.rs index 112ecdd7..bd81042e 100644 --- a/brane-shr/src/input.rs +++ b/brane-shr/src/input.rs @@ -25,7 +25,9 @@ use std::{error, mem}; use dialoguer::theme::ColorfulTheme; use dialoguer::{Completion, Confirm, History, Input, Select}; -use log::warn; +use log::{info, warn}; + +const HISTORY_CAPACITY: usize = 500; /***** ERRORS *****/ @@ -77,6 +79,20 @@ pub struct FileHistory { } impl FileHistory { + /// Create an empty File History + /// + /// # Arguments + /// - `path`: Points to the location of this history's file. + /// + /// # Returns + /// A new FileHistory instance. + pub fn new(path: impl Into) -> Self { + Self { + path: path.into(), + history: Default::default(), + } + } + /// Constructor for the FileHistory. /// /// Attempts to read the history from the given `path`, and writes it back when this struct is dropped (unless [`Self::forget()`] is called). To this end, avoid having two FileHistory's that point to the same file. @@ -89,22 +105,15 @@ impl FileHistory { /// /// # Warnings /// This function emits warnings using [`warn!()`] when it fails to read the file. - pub fn new(path: impl Into) -> Self { + pub fn from_file(path: impl Into) -> Result { let path: PathBuf = path.into(); // Attempt to read the file - let raw: String = match fs::read_to_string(&path) { - Ok(raw) => raw, - Err(err) => { - warn!("Failed to read history file '{}': {}", path.display(), err); - return Self { path, history: VecDeque::new() }; - }, - }; + let raw: String = fs::read_to_string(&path)?; // Store it as line-separated, and restore escaped characters let iter = raw.lines().map(|s| s.to_string()); - let size_hint = iter.size_hint(); - let mut history: VecDeque = VecDeque::with_capacity(size_hint.1.unwrap_or(size_hint.0)); + let mut history: VecDeque = Default::default(); for line in iter { // Deflate the special characters by parsing them let mut escaping: bool = false; @@ -127,7 +136,23 @@ impl FileHistory { } // We can now save the restored history - Self { path, history } + Ok(Self { path, history }) + } + + /// Read a file history from disk or create a new instance + /// + /// # Arguments + /// - `path`: Points to the location of this history's file. + /// + /// # Returns + /// A new FileHistory instance. + pub fn from_file_or_new(path: impl Into) -> Self { + let path: PathBuf = path.into(); + + Self::from_file(path.clone()).unwrap_or_else(|err| { + info!("Failed to read history file '{}': {}", path.display(), err); + Self::new(path) + }) } /// Drops this history without saving it. @@ -171,7 +196,7 @@ impl History for FileHistory { fn write(&mut self, val: &String) { // Pop the front if we don't have the space - while self.history.len() >= 500 { + while self.history.len() >= HISTORY_CAPACITY { self.history.pop_back(); } @@ -373,6 +398,71 @@ where } } +// This function is mostly necessary as we cannot implement FromStr for Option
. We could +// return a String and then parse it later again, but this has the downside that we cannot +// guarantee using the type system that this is sound, so we would need to unwrap. +// If somebody thinks of a way to integrate this directly into `input()`. I'm all for it. +// TODO: Documentation +pub fn input_option( + what: impl Display, + prompt: impl ToString, + default: Option>, + validator: Option, + mut history: Option>, +) -> Result, Error> +where + S: FromStr + ToString, + S::Err: error::Error, + VA: dialoguer::InputValidator, + VA::Err: ToString, +{ + // Preprocess the input + let mut prompt: String = prompt.to_string(); + let default: Option = default.map(|d| d.into()); + + // Loop until the user enters a valid value. + let theme: ColorfulTheme = ColorfulTheme::default(); + + // Construct the prompt + let mut input: Input = Input::with_theme(&theme); + if let Some(default) = &default { + input = input.default(default.to_string()); + } + if let Some(history) = &mut history { + input = input.history_with(history); + } + + if let Some(validator) = validator { + input = input.validate_with(validator); + } + + loop { + let input = input.clone().with_prompt(&prompt); + // Run the prompt + let res: String = match input.interact_text() { + Ok(res) => res, + Err(err) => { + return Err(Error::Text { err }); + }, + }; + + if res.trim().is_empty() { + return Ok(None); + } + + // Attempt to parse it as S + match S::from_str(&res) { + Ok(res) => { + return Ok(Some(res)); + }, + Err(err) => { + warn!("Failed to parse '{}' as {}: {}", res, std::any::type_name::(), err); + prompt = format!("Illegal value for {what}; try again"); + }, + } + } +} + /// Prompts the user for an input path. /// /// While [`input()`] can be used too, this function features auto-completion for the filesystem. @@ -596,6 +686,7 @@ where /// /// # Errors /// This function errors if we failed to interact with the user. +// TODO: This should be display preferable I would think pub fn select(prompt: impl ToString, options: impl IntoIterator, default: Option) -> Result { // Collect the options let mut options: Vec = options.into_iter().collect(); @@ -611,3 +702,33 @@ pub fn select(prompt: impl ToString, options: impl IntoIterator Err(Error::Select { n_opts: options.len(), err }), } } + +/// Prompts the user to select on the given values. +/// +/// # Arguments +/// - `prompt`: The prompt to display to the user. +/// - `options`: A list of options to select from. +/// - `default`: If not [`None`], then the select highlights another item than the first. +/// +/// # Returns +/// The selected option. If the user aborted the select, [`None`] is returned instead. +/// +/// # Errors +/// This function errors if we failed to interact with the user. +pub fn select_enum(prompt: impl ToString, options: impl IntoIterator, default: Option) -> Result { + // Collect the options + let mut options: Vec = options.into_iter().collect(); + + // Construct the prompt + let theme: ColorfulTheme = ColorfulTheme::default(); + let mut input: Select = Select::with_theme(&theme); + let default_index: usize = default.and_then(|item| options.iter().position(|elem| elem == &item)).unwrap_or(0); + + input = input.with_prompt(prompt.to_string()).default(default_index).items(&options).report(true); + + // Run it + match input.interact() { + Ok(index) => Ok(options.swap_remove(index)), + Err(err) => Err(Error::Select { n_opts: options.len(), err }), + } +} diff --git a/specifications/Cargo.toml b/specifications/Cargo.toml index 705e2e76..6f0f63fb 100644 --- a/specifications/Cargo.toml +++ b/specifications/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4.35", features = ["serde"] } const_format = "0.2.22" enum-debug.workspace = true futures = "0.3.24" +indoc = "2.0.5" # lazy_static = "1.4.0" jsonwebtoken = "9.2.0" log = "0.4.22" diff --git a/specifications/src/address.rs b/specifications/src/address.rs index ced23b2a..74a3ebfa 100644 --- a/specifications/src/address.rs +++ b/specifications/src/address.rs @@ -63,7 +63,57 @@ impl Error for AddressError { +#[derive(Clone, Debug, EnumDebug, Hash, PartialEq, Eq)] +pub enum Host { + Ipv4(Ipv4Addr), + Ipv6(Ipv6Addr), + Hostname(String), +} + +impl From for Host { + fn from(value: Ipv4Addr) -> Self { Self::Ipv4(value) } +} + +impl From for Host { + fn from(value: Ipv6Addr) -> Self { Self::Ipv6(value) } +} + +impl From<(Host, u16)> for Address { + fn from((host, port): (Host, u16)) -> Self { + match host { + Host::Ipv4(x) => Self::Ipv4(x, port), + Host::Ipv6(x) => Self::Ipv6(x, port), + Host::Hostname(x) => Self::Hostname(x, port), + } + } +} + +impl FromStr for Host { + type Err = std::convert::Infallible; + + fn from_str(value: &str) -> Result { + if let Ok(value) = value.parse::() { + return Ok(Self::Ipv4(value)); + } + + if let Ok(value) = value.parse::() { + return Ok(Self::Ipv6(value)); + } + + Ok(Self::Hostname(value.to_string())) + } +} +impl Display for Host { + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { + use Host::*; + match self { + Ipv4(addr) => write!(f, "{addr}"), + Ipv6(addr) => write!(f, "{addr}"), + Hostname(addr) => write!(f, "{addr}"), + } + } +} /***** LIBRARY *****/ /// Defines a more lenient alternative to a SocketAddr that also accepts hostnames. @@ -89,6 +139,8 @@ impl Address { /// # Returns /// A new Address instance. #[inline] + // TODO: Maybe it is better to just implement From<(Ipv4Addr>, u16)> + // Having n+1 constructors seems a bit excessive pub fn ipv4(b1: u8, b2: u8, b3: u8, b4: u8, port: u16) -> Self { Self::Ipv4(Ipv4Addr::new(b1, b2, b3, b4), port) } /// Constructor for the Address that initializes it for the given IPv4 address. diff --git a/specifications/src/constants/mod.rs b/specifications/src/constants/mod.rs new file mode 100644 index 00000000..2725942b --- /dev/null +++ b/specifications/src/constants/mod.rs @@ -0,0 +1,53 @@ +//! Constants +//! +//! This file is a collection of constants that may be used throughout the Brane codebase +//! Using this file we can change defaults on various parts of the infrastructure without modifying +//! a whole bunch of files and inevitably missing one. Some examples of things that should be +//! stored in here are URLs to the documentation. These can change easily and it would be +//! unforunate if it would require more changes than one. + +pub const BRANE_CENTRAL_PRX_PORT: u16 = 50050; +pub const BRANE_CENTRAL_API_PORT: u16 = 50051; +pub const BRANE_CENTRAL_PLR_PORT: u16 = 50052; +pub const BRANE_CENTRAL_DRV_PORT: u16 = 50053; + +pub const BRANE_WORKER_PRX_PORT: u16 = 50150; +pub const BRANE_WORKER_REG_PORT: u16 = 50151; +pub const BRANE_WORKER_JOB_PORT: u16 = 50152; +pub const BRANE_WORKER_CHK_PORT: u16 = 50153; + +pub const SCYLLA_PORT: u16 = 9042; + +pub const BRANE_CENTRAL_PRX_NAME: &str = "brane-prx"; +pub const BRANE_CENTRAL_API_NAME: &str = "brane-api"; +pub const BRANE_CENTRAL_PLR_NAME: &str = "brane-plr"; +pub const BRANE_CENTRAL_DRV_NAME: &str = "brane-drv"; + +// The only way this can be static path is using something lazy lock or lazy cell. +pub const CERTIFICATE_PATH: &str = "certs"; +pub const PACKAGE_PATH: &str = "packages"; + +pub const NODE_HEADER: &str = indoc::indoc!(" + # This file defines the environment of the local node. + # Edit this file to change service properties. Some require a restart + # of the service (typically any 'ports' or 'topics' related setting), but most + # will be reloaded dynamically by the services themselves. +"); + +pub const PROXY_HEADER: &str = indoc::indoc!(" + # This file defines the settings for the proxy service on this node. + # This file is loaded eagerly, so changing it requires a restart of the proxy + # service itself. +"); + +pub const INFRA_HEADER: &str = indoc::indoc!(" + # This file defines the nodes part of this Brane instance. + # Edit this file to change the location of nodes and relevant services. + # This file is loaded lazily, so changing it typically does not require a + # restart. +"); + +pub const INFRA_CONFIG_URL: &str = "https://wiki.enablingpersonalizedinterventions.nl/user-guide/system-admins/docs/config/infra.html"; +pub const PROXY_CONFIG_URL: &str = "https://wiki.enablingpersonalizedinterventions.nl/user-guide/system-admins/docs/config/proxy.html"; +pub const CENTRAL_NODE_CONFIG_URL: &str = "https://wiki.enablingpersonalizedinterventions.nl/user-guide/system-admins/docs/config/node.html"; +pub const WORKER_NODE_CONFIG_URL: &str = "https://wiki.enablingpersonalizedinterventions.nl/user-guide/system-admins/docs/config/node.html"; diff --git a/specifications/src/lib.rs b/specifications/src/lib.rs index 071ce5bc..eca60298 100644 --- a/specifications/src/lib.rs +++ b/specifications/src/lib.rs @@ -20,6 +20,7 @@ pub mod address; pub mod arch; pub mod checking; pub mod common; +pub mod constants; pub mod container; pub mod data; pub mod driving;