From d4d5c5dd29b40e9c563197490adce503c1370be8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 13 Jan 2017 16:45:46 -0800 Subject: [PATCH] Override config parameters via environment variables. Resolves #37. --- lib/src/config/builder.rs | 70 +---------- lib/src/config/error.rs | 10 ++ lib/src/config/mod.rs | 240 ++++++++++++++++++++++++++++++------- lib/src/config/toml_ext.rs | 87 ++++++++++++++ lib/src/logger.rs | 13 ++ lib/src/rocket.rs | 8 +- 6 files changed, 316 insertions(+), 112 deletions(-) create mode 100644 lib/src/config/toml_ext.rs diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index c09b6695..c49e35b1 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -1,7 +1,7 @@ -use std::collections::{HashMap, BTreeMap}; -use std::hash::Hash; +use std::collections::HashMap; use config::{Result, Config, Value, Environment}; +use config::toml_ext::IntoValue; use logger::LoggingLevel; /// The core configuration structure. @@ -111,69 +111,3 @@ impl ConfigBuilder { self.finalize().expect("ConfigBuilder::unwrap() failed") } } - -pub trait IntoValue { - fn into_value(self) -> Value; -} - -impl<'a> IntoValue for &'a str { - fn into_value(self) -> Value { - Value::String(self.to_string()) - } -} - -impl IntoValue for Value { - fn into_value(self) -> Value { - self - } -} - -impl IntoValue for Vec { - fn into_value(self) -> Value { - Value::Array(self.into_iter().map(|v| v.into_value()).collect()) - } -} - -impl, V: IntoValue> IntoValue for BTreeMap { - fn into_value(self) -> Value { - let table = self.into_iter() - .map(|(s, v)| (s.into(), v.into_value())) - .collect(); - - Value::Table(table) - } -} - -impl + Hash + Eq, V: IntoValue> IntoValue for HashMap { - fn into_value(self) -> Value { - let table = self.into_iter() - .map(|(s, v)| (s.into(), v.into_value())) - .collect(); - - Value::Table(table) - } -} - -macro_rules! impl_into_value { - ($variant:ident : $t:ty) => ( impl_into_value!($variant: $t,); ); - - ($variant:ident : $t:ty, $($extra:tt)*) => ( - impl IntoValue for $t { - fn into_value(self) -> Value { - Value::$variant(self $($extra)*) - } - } - ) -} - -impl_into_value!(String: String); -impl_into_value!(Integer: i64); -impl_into_value!(Integer: isize, as i64); -impl_into_value!(Integer: i32, as i64); -impl_into_value!(Integer: i8, as i64); -impl_into_value!(Integer: u8, as i64); -impl_into_value!(Integer: u32, as i64); -impl_into_value!(Boolean: bool); -impl_into_value!(Float: f64); -impl_into_value!(Float: f32, as f64); - diff --git a/lib/src/config/error.rs b/lib/src/config/error.rs index eeb6015e..f125724f 100644 --- a/lib/src/config/error.rs +++ b/lib/src/config/error.rs @@ -47,6 +47,10 @@ pub enum ConfigError { /// /// Parameters: (toml_source_string, filename, error_list) ParseError(String, PathBuf, Vec), + /// There was a TOML parsing error in a config environment variable. + /// + /// Parameters: (env_key, env_value, expected type) + BadEnvVal(String, String, &'static str), } impl ConfigError { @@ -90,6 +94,12 @@ impl ConfigError { trace_!("'{}' - {}", error_source, White.paint(&error.desc)); } } + BadEnvVal(ref key, ref value, ref expected) => { + error!("environment variable '{}={}' could not be parsed", + White.paint(key), White.paint(value)); + info_!("value for {:?} must be {}", + White.paint(key), White.paint(expected)) + } } } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index e641cc09..516d3261 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -101,6 +101,20 @@ //! address = "0.0.0.0" //! ``` //! +//! ## Environment Variables +//! +//! All configuration parameters, including extras, can be overridden through +//! environment variables. To override the configuration parameter `param`, use +//! an environment variable named `ROCKET_PARAM`. For instance, to override the +//! "port" configuration parameter, you can run your application with: +//! +//! ```sh +//! ROCKET_PORT=3721 ./your_application +//! ``` +//! +//! Environment variables take precedence over all other configuration methods: +//! if the variable is set, it will be used as the value for the parameter. +//! //! ## Retrieving Configuration Parameters //! //! Configuration parameters for the currently active configuration environment @@ -133,6 +147,7 @@ mod error; mod environment; mod config; mod builder; +mod toml_ext; use std::sync::{Once, ONCE_INIT}; use std::fs::{self, File}; @@ -149,15 +164,21 @@ pub use self::error::{ConfigError, ParsingError}; pub use self::environment::Environment; pub use self::config::Config; pub use self::builder::ConfigBuilder; +pub use self::toml_ext::IntoValue; use self::Environment::*; +use self::environment::CONFIG_ENV; +use self::toml_ext::parse_simple_toml_value; use logger::{self, LoggingLevel}; +use http::ascii::uncased_eq; static INIT: Once = ONCE_INIT; static mut CONFIG: Option = None; const CONFIG_FILENAME: &'static str = "Rocket.toml"; const GLOBAL_ENV_NAME: &'static str = "global"; +const ENV_VAR_PREFIX: &'static str = "ROCKET_"; +const PREHANDLED_VARS: [&'static str; 2] = ["ROCKET_CODEGEN_DEBUG", CONFIG_ENV]; /// Wraps `std::result` with the error type of /// [ConfigError](enum.ConfigError.html). @@ -171,8 +192,15 @@ pub struct RocketConfig { } impl RocketConfig { - /// TODO: Doc. - fn new(config: Config) -> RocketConfig { + /// Create a new configuration using the passed in `config` for all + /// environments. The Rocket.toml file is ignored, as are environment + /// variables. + /// + /// # Panics + /// + /// If the current working directory can't be retrieved, this function + /// panics. + pub fn new(config: Config) -> RocketConfig { let f = config.config_path.clone(); let active_env = config.environment; @@ -190,6 +218,41 @@ impl RocketConfig { } } + /// Read the configuration from the `Rocket.toml` file. The file is search + /// for recursively up the tree, starting from the CWD. + pub fn read() -> Result { + // Find the config file, starting from the `cwd` and working backwords. + let file = RocketConfig::find()?; + + // Try to open the config file for reading. + let mut handle = File::open(&file).map_err(|_| ConfigError::IOError)?; + + // Read the configure file to a string for parsing. + let mut contents = String::new(); + handle.read_to_string(&mut contents).map_err(|_| ConfigError::IOError)?; + + // Parse the config and return the result. + RocketConfig::parse(contents, &file) + } + + /// Return the default configuration for all environments and marks the + /// active environment (via the CONFIG_ENV variable) as active. + pub fn active_default>(filename: P) -> Result { + let mut defaults = HashMap::new(); + defaults.insert(Development, Config::default_for(Development, &filename)?); + defaults.insert(Staging, Config::default_for(Staging, &filename)?); + defaults.insert(Production, Config::default_for(Production, &filename)?); + + let mut config = RocketConfig { + active_env: Environment::active()?, + config: defaults, + }; + + // Override any variables from the environment. + config.override_from_env()?; + Ok(config) + } + /// Iteratively search for `CONFIG_FILENAME` starting at the current working /// directory and working up through its parents. Returns the path to the /// file or an Error::NoKey if the file couldn't be found. If the current @@ -213,18 +276,20 @@ impl RocketConfig { Err(ConfigError::NotFound) } + fn get_mut(&mut self, env: Environment) -> &mut Config { + match self.config.get_mut(&env) { + Some(config) => config, + None => panic!("set(): {} config is missing.", env), + } + } + /// Set the configuration for the environment `env` to be the configuration /// derived from the TOML table `kvs`. The environment must already exist in /// `self`, otherwise this function panics. Any existing values are /// overriden by those in `kvs`. - fn set(&mut self, env: Environment, kvs: &Table) -> Result<()> { - let config = match self.config.get_mut(&env) { - Some(config) => config, - None => panic!("set(): {} config is missing.", env), - }; - + fn set_from_table(&mut self, env: Environment, kvs: &Table) -> Result<()> { for (key, value) in kvs { - config.set(key, value)?; + self.get_mut(env).set(key, value)?; } Ok(()) @@ -243,6 +308,41 @@ impl RocketConfig { self.get(self.active_env) } + // Override all environments with values from env variables if present. + fn override_from_env(&mut self) -> Result<()> { + 'outer: for (env_key, env_val) in env::vars() { + if env_key.len() < ENV_VAR_PREFIX.len() { + continue + } else if !uncased_eq(&env_key[..ENV_VAR_PREFIX.len()], ENV_VAR_PREFIX) { + continue + } + + // Skip environment variables that are handled elsewhere. + for prehandled_var in PREHANDLED_VARS.iter() { + if uncased_eq(&env_key, &prehandled_var) { + continue 'outer + } + } + + // Parse the key and value and try to set the variable for all envs. + let key = env_key[ENV_VAR_PREFIX.len()..].to_lowercase(); + let val = parse_simple_toml_value(&env_val); + for env in &Environment::all() { + match self.get_mut(*env).set(&key, &val) { + Err(ConfigError::BadType(_, exp, _, _)) => { + return Err(ConfigError::BadEnvVal(env_key, env_val, exp)) + } + Err(e) => return Err(e), + Ok(_) => { /* move along */ } + } + } + } + + Ok(()) + } + + /// Parses the configuration from the Rocket.toml file. Also overrides any + /// values there with values from the environment. fn parse>(src: String, filename: P) -> Result { // Get a PathBuf version of the filename. let path = filename.as_ref().to_path_buf(); @@ -287,7 +387,7 @@ impl RocketConfig { // This is not the global table. Parse the environment name from the // table entry name and then set all of the key/values. match entry.as_str().parse() { - Ok(env) => config.set(env, kv_pairs)?, + Ok(env) => config.set_from_table(env, kv_pairs)?, Err(_) => Err(ConfigError::BadEntry(entry.clone(), path.clone()))? } } @@ -295,39 +395,15 @@ impl RocketConfig { // Override all of the environments with the global values. if let Some(ref global_kv_pairs) = global { for env in &Environment::all() { - config.set(*env, global_kv_pairs)?; + config.set_from_table(*env, global_kv_pairs)?; } } + // Override any variables from the environment. + config.override_from_env()?; + Ok(config) } - - pub fn read() -> Result { - // Find the config file, starting from the `cwd` and working backwords. - let file = RocketConfig::find()?; - - // Try to open the config file for reading. - let mut handle = File::open(&file).map_err(|_| ConfigError::IOError)?; - - // Read the configure file to a string for parsing. - let mut contents = String::new(); - handle.read_to_string(&mut contents).map_err(|_| ConfigError::IOError)?; - - // Parse the contents from the file. - RocketConfig::parse(contents, &file) - } - - pub fn active_default>(filename: P) -> Result { - let mut defaults = HashMap::new(); - defaults.insert(Development, Config::default_for(Development, &filename)?); - defaults.insert(Staging, Config::default_for(Staging, &filename)?); - defaults.insert(Production, Config::default_for(Production, &filename)?); - - Ok(RocketConfig { - active_env: Environment::active()?, - config: defaults, - }) - } } /// Returns the active configuration and whether this call initialized the @@ -382,7 +458,7 @@ unsafe fn private_init() { let config = RocketConfig::read().unwrap_or_else(|e| { match e { ParseError(..) | BadEntry(..) | BadEnv(..) | BadType(..) - | BadFilePath(..) => bail(e), + | BadFilePath(..) | BadEnvVal(..) => bail(e), IOError | BadCWD => warn!("Failed reading Rocket.toml. Using defaults."), NotFound => { /* try using the default below */ } } @@ -412,7 +488,7 @@ mod test { use std::env; use std::sync::Mutex; - use super::{RocketConfig, ConfigError, ConfigBuilder}; + use super::{RocketConfig, Config, ConfigError, ConfigBuilder}; use super::{Environment, GLOBAL_ENV_NAME}; use super::environment::CONFIG_ENV; use super::Environment::*; @@ -876,4 +952,88 @@ mod test { }); } } + + #[test] + fn test_env_override() { + // Take the lock so changing the environment doesn't cause races. + let _env_lock = ENV_LOCK.lock().unwrap(); + + let pairs = [ + ("log", "critical"), ("LOG", "debug"), ("PORT", "8110"), + ("address", "1.2.3.4"), ("EXTRA_EXTRA", "true"), ("workers", "3") + ]; + + let check_value = |key: &str, val: &str, config: &Config| { + match key { + "log" => assert_eq!(config.log_level, val.parse().unwrap()), + "port" => assert_eq!(config.port, val.parse().unwrap()), + "address" => assert_eq!(config.address, val), + "extra_extra" => assert_eq!(config.get_bool(key).unwrap(), true), + "workers" => assert_eq!(config.workers, val.parse().unwrap()), + _ => panic!("Unexpected key: {}", key) + } + }; + + // Check that setting the environment variable actually changes the + // config for the default active and nonactive environments. + for &(key, val) in &pairs { + env::set_var(format!("ROCKET_{}", key), val); + + let rconfig = active_default().unwrap(); + // Check that it overrides the active config. + for env in &Environment::all() { + env::set_var(CONFIG_ENV, env.to_string()); + let rconfig = active_default().unwrap(); + check_value(&*key.to_lowercase(), val, rconfig.active()); + } + + // And non-active configs. + for env in &Environment::all() { + check_value(&*key.to_lowercase(), val, rconfig.get(*env)); + } + } + + // Clear the variables so they don't override for the next test. + for &(key, _) in &pairs { + env::remove_var(format!("ROCKET_{}", key)) + } + + // Now we build a config file to test that the environment variables + // override configurations from files as well. + let toml = r#" + [dev] + address = "1.2.3.4" + + [stage] + address = "2.3.4.5" + + [prod] + address = "10.1.1.1" + + [global] + address = "1.2.3.4" + port = 7810 + workers = 21 + log = "normal" + "#.to_string(); + + // Check that setting the environment variable actually changes the + // config for the default active environments. + for &(key, val) in &pairs { + env::set_var(format!("ROCKET_{}", key), val); + + let r = RocketConfig::parse(toml.clone(), TEST_CONFIG_FILENAME).unwrap(); + check_value(&*key.to_lowercase(), val, r.active()); + + // And non-active configs. + for env in &Environment::all() { + check_value(&*key.to_lowercase(), val, r.get(*env)); + } + } + + // Clear the variables so they don't override for the next test. + for &(key, _) in &pairs { + env::remove_var(format!("ROCKET_{}", key)) + } + } } diff --git a/lib/src/config/toml_ext.rs b/lib/src/config/toml_ext.rs new file mode 100644 index 00000000..4aeceeb5 --- /dev/null +++ b/lib/src/config/toml_ext.rs @@ -0,0 +1,87 @@ +use std::collections::{HashMap, BTreeMap}; +use std::hash::Hash; +use std::str::FromStr; + +use config::Value; + +pub fn parse_simple_toml_value(string: &str) -> Value { + if let Ok(int) = i64::from_str(string) { + return Value::Integer(int) + } + + if let Ok(boolean) = bool::from_str(string) { + return Value::Boolean(boolean) + } + + if let Ok(float) = f64::from_str(string) { + return Value::Float(float) + } + + Value::String(string.to_string()) +} + +pub trait IntoValue { + fn into_value(self) -> Value; +} + +impl<'a> IntoValue for &'a str { + fn into_value(self) -> Value { + Value::String(self.to_string()) + } +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl IntoValue for Vec { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(|v| v.into_value()).collect()) + } +} + +impl, V: IntoValue> IntoValue for BTreeMap { + fn into_value(self) -> Value { + let table = self.into_iter() + .map(|(s, v)| (s.into(), v.into_value())) + .collect(); + + Value::Table(table) + } +} + +impl + Hash + Eq, V: IntoValue> IntoValue for HashMap { + fn into_value(self) -> Value { + let table = self.into_iter() + .map(|(s, v)| (s.into(), v.into_value())) + .collect(); + + Value::Table(table) + } +} + +macro_rules! impl_into_value { + ($variant:ident : $t:ty) => ( impl_into_value!($variant: $t,); ); + + ($variant:ident : $t:ty, $($extra:tt)*) => ( + impl IntoValue for $t { + fn into_value(self) -> Value { + Value::$variant(self $($extra)*) + } + } + ) +} + +impl_into_value!(String: String); +impl_into_value!(Integer: i64); +impl_into_value!(Integer: isize, as i64); +impl_into_value!(Integer: i32, as i64); +impl_into_value!(Integer: i8, as i64); +impl_into_value!(Integer: u8, as i64); +impl_into_value!(Integer: u32, as i64); +impl_into_value!(Boolean: bool); +impl_into_value!(Float: f64); +impl_into_value!(Float: f32, as f64); + diff --git a/lib/src/logger.rs b/lib/src/logger.rs index df4a1954..39ec56f7 100644 --- a/lib/src/logger.rs +++ b/lib/src/logger.rs @@ -1,6 +1,7 @@ //! Rocket's logging infrastructure. use std::str::FromStr; +use std::fmt; use log::{self, Log, LogLevel, LogRecord, LogMetadata}; use term_painter::Color::*; @@ -44,6 +45,18 @@ impl FromStr for LoggingLevel { } } +impl fmt::Display for LoggingLevel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let string = match *self { + LoggingLevel::Critical => "critical", + LoggingLevel::Normal => "normal", + LoggingLevel::Debug => "debug", + }; + + write!(f, "{}", string) + } +} + #[doc(hidden)] #[macro_export] macro_rules! log_ { ($name:ident: $format:expr) => { log_!($name: $format,) }; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 17d22c81..25b80cb0 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -300,6 +300,7 @@ impl Rocket { /// Creates a new `Rocket` application using the supplied custom /// configuration information. The `Rocket.toml` file, if present, is + /// ignored. Any environment variables setting config parameters are /// ignored. If `log` is `true`, logging is enabled. /// /// This method is typically called through the `rocket::custom` alias. @@ -331,10 +332,9 @@ impl Rocket { } info!("🔧 Configured for {}.", config.environment); - info_!("listening: {}:{}", - White.paint(&config.address), - White.paint(&config.port)); - info_!("logging: {:?}", White.paint(config.log_level)); + info_!("address: {}", White.paint(&config.address)); + info_!("port: {}", White.paint(&config.port)); + info_!("log: {}", White.paint(config.log_level)); info_!("workers: {}", White.paint(config.workers)); let session_key = config.take_session_key();