diff --git a/configuration/Cargo.toml b/configuration/Cargo.toml index 12d0696..466fad8 100644 --- a/configuration/Cargo.toml +++ b/configuration/Cargo.toml @@ -6,8 +6,10 @@ edition = "2018" description = "Configuration gathering for factomd" [dependencies] -clap = { version = "2.32.0", default-features = false } +clap = { version = "2.32.0", features = ["yaml"] } structopt = "0.2.18" config = "0.9.3" slog = "^2" serde = { version = "1.0", features = ["derive"] } +strum = "0.15.0" +strum_macros = "0.15.0" \ No newline at end of file diff --git a/configuration/cli.yml b/configuration/cli.yml new file mode 100644 index 0000000..8998e8b --- /dev/null +++ b/configuration/cli.yml @@ -0,0 +1,45 @@ +name: Factom +args: + - config: + short: c + long: config + value_name: FILE + help: Sets a custom config file + takes_value: true + - network: + short: n + long: network + takes_value: true + - role: + short: r + long: role + possible_values: [AUTHORITY, LIGHT, FULL] + takes_value: true + - node_key_env: + short: k + long: node_key_env + takes_value: true + - walletd_user: + long: walletd-user + takes_value: true + - walletd_env_var: + long: walletd-env-var + - disable_rpc: + short: d + long: disable-rpc + takes_value: true + - rpc_addr: + long: rpc-addr + takes_value: true + - rpc_port: + long: rpc-port + takes_value: true + - log_level: + short: l + long: log-level + possible_values: [OFF, CRITICAL, ERROR, WARN, INFO, DEBUG, TRACE] + takes_value: true + - completions: + long: completions + possible_values: [bash, fish, zsh, powershell, elvish] + takes_value: true \ No newline at end of file diff --git a/configuration/src/lib.rs b/configuration/src/lib.rs index 60c1172..5b2a353 100644 --- a/configuration/src/lib.rs +++ b/configuration/src/lib.rs @@ -1,26 +1,27 @@ //! # Factomd Configuration -//! +//! //! At present we are using structopt and clap to build up a layered configuration. //! See the docs for [config](https://docs.rs/config/0.9.3/config/). Config generates //! bash completion scripts and layers different vectors for configuration (env vars, files, etc). -//! +//! //! Structopt takes a datastructure and turns it into the `--help` command and then provides all //! cli parsing we need. -//! -//! These will provide factomd with all the configurations provided by the user, for exmaple: using env var for +//! +//! These will provide factomd with all the configurations provided by the user, for exmaple: using env var for //! walletd password, using file for node rpc settings, using cli for overrides. -//! -extern crate config; +//! +#[macro_use] extern crate clap; -extern crate structopt; +extern crate config; extern crate serde; +extern crate structopt; +use clap::{App, _clap_count_exprs, arg_enum}; +use config::{Config, ConfigError, Environment, File}; +use serde::Deserialize; use std::io; use structopt::clap::Shell; -use clap::{arg_enum, _clap_count_exprs}; use structopt::StructOpt; -use serde::{Deserialize}; -use config::{ConfigError, Config, File, Environment,}; // Log Levels Enum arg_enum! { @@ -50,11 +51,12 @@ arg_enum! { #[derive(StructOpt, Debug, Deserialize)] pub struct Log { #[structopt( - short = "l", - long = "log-level", - default_value = "DEBUG", - raw(possible_values = "&LogLevel::variants()", case_insensitive = "false"))] - pub log_level: LogLevel + short = "l", + long = "log-level", + default_value = "DEBUG", + raw(possible_values = "&LogLevel::variants()", case_insensitive = "false") + )] + pub log_level: LogLevel, } /// Factom RPC API settings @@ -64,7 +66,7 @@ pub struct Rpc { #[structopt(short = "d", long = "disable-rpc")] pub disable_rpc: bool, - /// HTTP-RPC listening interface + /// HTTP-RPC listening interface #[structopt(long = "rpc-addr", default_value = "127.0.0.1")] pub rpc_addr: String, @@ -77,7 +79,7 @@ pub struct Rpc { /// Takes a env var for password, may use a password entry input in the future #[derive(StructOpt, Debug, Deserialize)] pub struct Walletd { - /// Set walletd user for authentication + /// Set walletd user for authentication #[structopt(long = "walletd-user", default_value = "")] pub walletd_user: String, @@ -94,15 +96,19 @@ pub struct Server { pub network: String, /// Environment variable to source for your node_key - #[structopt (long = "role", short = "r", default_value = "", raw(possible_values = "&Role::variants()", case_insensitive = "false"))] + #[structopt( + long = "role", + short = "r", + default_value = "", + raw(possible_values = "&Role::variants()", case_insensitive = "false") + )] pub role: Role, /// Environment variable to source for your node_key - #[structopt (long = "node_key_env", short = "k", default_value = "")] + #[structopt(long = "node_key_env", short = "k", default_value = "")] pub node_key_env: String, } - /// FactomConfig used for setting up your Factom node #[derive(StructOpt, Debug, Deserialize)] #[structopt(name = "Factom")] @@ -112,23 +118,27 @@ pub struct FactomConfig { pub custom_config: String, #[allow(missing_docs)] - #[structopt(flatten)] - pub server: Server, + #[structopt(flatten)] + pub server: Server, #[allow(missing_docs)] - #[structopt(flatten)] + #[structopt(flatten)] pub log: Log, #[allow(missing_docs)] - #[structopt(flatten)] + #[structopt(flatten)] pub rpc: Rpc, #[allow(missing_docs)] - #[structopt(flatten)] + #[structopt(flatten)] pub walletd: Walletd, /// Generate completions - #[structopt (long = "completions", default_value = "", raw(possible_values = "&Shell::variants()", case_insensitive = "false"))] + #[structopt( + long = "completions", + default_value = "", + raw(possible_values = "&Shell::variants()", case_insensitive = "false") + )] pub completions: String, } @@ -136,48 +146,108 @@ pub struct FactomConfig { /// Higher precedence -> lower precedence /// CLI Args -> Environment Vars -> Custom Config -> default_config.yml impl FactomConfig { - /// Generate a new FactomConfig from multiple sources - /// - /// config-rs is mutable, must have CLI gathered to start, a lot of mutation and side effects not avoidable - pub fn new() -> Result { - // Just to get a custom config path - let cli_args = FactomConfig::from_args(); - - // Dump shell completion to stdout - if !cli_args.completions.is_empty() { - FactomConfig::completions_to_stdout(&cli_args.completions); - } + /// Generate a new FactomConfig from multiple sources + /// + /// config-rs is mutable, must have CLI gathered to start, a lot of mutation and side effects not avoidable + pub fn new() -> Result { + let yaml = load_yaml!("../cli.yml"); + let matches = App::from_yaml(yaml).get_matches(); - // TODO Layer cli_args on top of here see github issue #1 - FactomConfig::load_from_path(&cli_args.custom_config) - } + // Just to get a custom config path + let cli_args = FactomConfig::from_args(); - pub fn load_from_path(path: &str) -> Result { - let mut settings = Config::new(); - // Get defaults - settings.merge(File::with_name("default_config.yml"))?; + // Dump shell completion to stdout + if !cli_args.completions.is_empty() { + FactomConfig::completions_to_stdout(&cli_args.completions); + } - settings.set("custom_config", "")?; - settings.set("completions", "")?; + let file_args = FactomConfig::load_from_path(&cli_args.custom_config)?; + let final_config = FactomConfig::check_cli(matches, file_args); - // If custom_config isn't an empty string, put the custom config - // on top of the default - if !path.is_empty() { - settings.merge(File::with_name(path))?; + Ok(final_config) } - // Read any configs in environment, search any prefixed FACTOMD_ - settings.merge(Environment::with_prefix("factomd"))?; + pub fn load_from_path(path: &str) -> Result { + let mut settings = Config::new(); + // Get defaults + settings.merge(File::with_name("default_config.yml"))?; - settings.try_into() - } + settings.set("custom_config", "")?; + settings.set("completions", "")?; - pub fn completions_to_stdout(shell: &str) { - let selected: Shell = shell.parse().unwrap(); - FactomConfig::clap().gen_completions_to(env!("CARGO_PKG_NAME"), selected, &mut io::stdout()); - } -} + // If custom_config isn't an empty string, put the custom config + // on top of the default + if !path.is_empty() { + settings.merge(File::with_name(path))?; + } + // Read any configs in environment, search any prefixed FACTOMD_ + settings.merge(Environment::with_prefix("factomd"))?; + + settings.try_into() + } + + /// Check for supplied command line arguments and use them to overwrite config values from files. + pub fn check_cli(matches: clap::ArgMatches, mut config: FactomConfig) -> FactomConfig { + if matches.occurrences_of("network") > 0 { + if let Some(value) = matches.value_of("network") { + config.server.network = value.to_string(); + } + } + if matches.occurrences_of("role") > 0 { + if let Some(value) = matches.value_of("role") { + config.server.role = value.parse::().expect("Invalid role type!"); + } + } + if matches.occurrences_of("node_key_env") > 0 { + if let Some(value) = matches.value_of("node_key_env") { + config.server.node_key_env = value.to_string(); + } + } + if matches.occurrences_of("log_level") > 0 { + if let Some(value) = matches.value_of("log_level") { + config.log.log_level = value.parse::().expect("Invalid log level!"); + } + } + if matches.occurrences_of("walletd_user") > 0 { + if let Some(value) = matches.value_of("walletd_user") { + config.walletd.walletd_user = value.to_string(); + } + } + if matches.occurrences_of("walletd_env_var") > 0 { + if let Some(value) = matches.value_of("walletd_env_var") { + config.walletd.walletd_env_var = value.to_string(); + } + } + if matches.occurrences_of("rpc_addr") > 0 { + if let Some(value) = matches.value_of("rpc_addr") { + config.rpc.rpc_addr = value.to_string(); + } + } + if matches.occurrences_of("rpc_port") > 0 { + if let Some(value) = matches.value_of("rpc_port") { + config.rpc.rpc_port = value.parse::().expect("Invalid port value!"); + } + } + if matches.occurrences_of("disable_rpc") > 0 { + if let Some(value) = matches.value_of("disable_rpc") { + config.rpc.disable_rpc = value + .parse::() + .expect("`disable_rpc` must be a bool!"); + } + } + config + } + + pub fn completions_to_stdout(shell: &str) { + let selected: Shell = shell.parse().unwrap(); + FactomConfig::clap().gen_completions_to( + env!("CARGO_PKG_NAME"), + selected, + &mut io::stdout(), + ); + } +} // These tests can be moved to test dir if needed #[cfg(test)] @@ -196,4 +266,4 @@ mod tests { assert_eq!(nondefault_config.server.role, Role::LIGHT); assert_eq!(nondefault_config.server.node_key_env, "FACTOMD_NODE_KEY"); } -} \ No newline at end of file +}