Override config parameters via environment variables.

Resolves #37.
This commit is contained in:
Sergio Benitez 2017-01-13 16:45:46 -08:00
parent 4bc5c20a45
commit d4d5c5dd29
6 changed files with 316 additions and 112 deletions

View File

@ -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<V: IntoValue> IntoValue for Vec<V> {
fn into_value(self) -> Value {
Value::Array(self.into_iter().map(|v| v.into_value()).collect())
}
}
impl<S: Into<String>, V: IntoValue> IntoValue for BTreeMap<S, V> {
fn into_value(self) -> Value {
let table = self.into_iter()
.map(|(s, v)| (s.into(), v.into_value()))
.collect();
Value::Table(table)
}
}
impl<S: Into<String> + Hash + Eq, V: IntoValue> IntoValue for HashMap<S, V> {
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);

View File

@ -47,6 +47,10 @@ pub enum ConfigError {
///
/// Parameters: (toml_source_string, filename, error_list)
ParseError(String, PathBuf, Vec<ParsingError>),
/// 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))
}
}
}

View File

@ -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<RocketConfig> = 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<RocketConfig> {
// 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<P: AsRef<Path>>(filename: P) -> Result<RocketConfig> {
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<P: AsRef<Path>>(src: String, filename: P) -> Result<RocketConfig> {
// 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<RocketConfig> {
// 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<P: AsRef<Path>>(filename: P) -> Result<RocketConfig> {
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))
}
}
}

View File

@ -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<V: IntoValue> IntoValue for Vec<V> {
fn into_value(self) -> Value {
Value::Array(self.into_iter().map(|v| v.into_value()).collect())
}
}
impl<S: Into<String>, V: IntoValue> IntoValue for BTreeMap<S, V> {
fn into_value(self) -> Value {
let table = self.into_iter()
.map(|(s, v)| (s.into(), v.into_value()))
.collect();
Value::Table(table)
}
}
impl<S: Into<String> + Hash + Eq, V: IntoValue> IntoValue for HashMap<S, V> {
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);

View File

@ -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,) };

View File

@ -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();