Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 1/merge cli config #27

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
45 changes: 45 additions & 0 deletions configuration/cli.yml
Original file line number Diff line number Diff line change
@@ -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
190 changes: 130 additions & 60 deletions configuration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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! {
Expand Down Expand Up @@ -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
Expand All @@ -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,

Expand All @@ -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,

Expand All @@ -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")]
Expand All @@ -112,72 +118,136 @@ 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,
}

/// Factom Configuration has a specific override order
/// 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<Self, ConfigError> {
// 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<Self, ConfigError> {
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<Self, ConfigError> {
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<Self, ConfigError> {
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::<Role>().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::<LogLevel>().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::<u16>().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::<bool>()
.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)]
Expand All @@ -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");
}
}
}