From 947dd263d7d71ea05564d1ecd5ce0f3dbd754356 Mon Sep 17 00:00:00 2001 From: Caio Date: Wed, 27 May 2020 12:43:29 -0300 Subject: [PATCH] Setup from env var (#103) --- refinery_cli/src/cli.rs | 6 +++ refinery_cli/src/migrate.rs | 30 +++++++++++---- refinery_core/Cargo.toml | 11 +++--- refinery_core/src/config.rs | 67 ++++++++++++++++++++++++++++++++- refinery_core/src/traits/mod.rs | 2 +- 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/refinery_cli/src/cli.rs b/refinery_cli/src/cli.rs index 0290842a..789025f1 100644 --- a/refinery_cli/src/cli.rs +++ b/refinery_cli/src/cli.rs @@ -17,6 +17,12 @@ pub fn create_cli() -> App<'static, 'static> { .help("give a config file location") .default_value("./refinery.toml"), ) + .arg( + Arg::with_name("env-var") + .short("e") + .help("if specified, loads database configuration from the given environment variable") + .takes_value(true), + ) .arg( Arg::with_name("grouped") .short("g") diff --git a/refinery_cli/src/migrate.rs b/refinery_cli/src/migrate.rs index b8cab87e..c3ecc194 100644 --- a/refinery_cli/src/migrate.rs +++ b/refinery_cli/src/migrate.rs @@ -1,20 +1,26 @@ use std::path::Path; -use anyhow::{Context, Result}; +use anyhow::Context; use clap::ArgMatches; use refinery_core::{config::Config, find_migration_files, Migration, MigrationType, Runner}; -pub fn handle_migration_command(args: &ArgMatches) -> Result<()> { +pub fn handle_migration_command(args: &ArgMatches) -> anyhow::Result<()> { //safe to call unwrap as we specified default values let config_location = args.value_of("config").unwrap(); let grouped = args.is_present("grouped"); let divergent = !args.is_present("divergent"); let missing = !args.is_present("missing"); + let env_var_opt = args.value_of("env-var"); match args.subcommand() { - ("files", Some(args)) => { - run_files_migrations(config_location, grouped, divergent, missing, args)? - } + ("files", Some(args)) => run_files_migrations( + config_location, + grouped, + divergent, + missing, + env_var_opt, + args, + )?, _ => unreachable!("Can't touch this..."), } Ok(()) @@ -25,8 +31,9 @@ fn run_files_migrations( grouped: bool, divergent: bool, missing: bool, + env_var_opt: Option<&str>, arg: &ArgMatches, -) -> Result<()> { +) -> anyhow::Result<()> { //safe to call unwrap as we specified default value let path = arg.value_of("path").unwrap(); let path = Path::new(path); @@ -46,8 +53,7 @@ fn run_files_migrations( .with_context(|| format!("could not read migration file name {}", path.display()))?; migrations.push(migration); } - let mut config = - Config::from_file_location(config_location).context("could not parse the config file")?; + let mut config = config(config_location, env_var_opt)?; Runner::new(&migrations) .set_grouped(grouped) .set_abort_divergent(divergent) @@ -55,3 +61,11 @@ fn run_files_migrations( .run(&mut config)?; Ok(()) } + +fn config(config_location: &str, env_var_opt: Option<&str>) -> anyhow::Result { + if let Some(env_var) = env_var_opt { + Config::from_env_var(env_var).context("could not environemnt variable") + } else { + Config::from_file_location(config_location).context("could not parse the config file") + } +} diff --git a/refinery_core/Cargo.toml b/refinery_core/Cargo.toml index cf7cc1c8..f6dcec89 100644 --- a/refinery_core/Cargo.toml +++ b/refinery_core/Cargo.toml @@ -13,16 +13,17 @@ default = [] rusqlite-bundled = ["rusqlite", "rusqlite/bundled"] [dependencies] +async-trait = "0.1" +cfg-if = "0.1.10" +chrono = "0.4" lazy_static = "1" -regex = "1" log = "0.4" -chrono = "0.4" +regex = "1" serde = { version = "1", features = ["derive"] } -cfg-if = "0.1.10" +siphasher = "0.3" thiserror = "1" -async-trait = "0.1" toml = "0.5" -siphasher = "0.3" +url = "2.0" walkdir = "2.3.1" rusqlite = {version = "0.23", optional = true} diff --git a/refinery_core/src/config.rs b/refinery_core/src/config.rs index 38668952..d311c5cb 100644 --- a/refinery_core/src/config.rs +++ b/refinery_core/src/config.rs @@ -35,6 +35,50 @@ impl Config { } } + /// create a new Config instance from an environment variable that contains an URL + pub fn from_env_var(name: &str) -> Result { + let value = std::env::var(name).map_err(|_| { + Error::new( + Kind::ConfigError(format!("Couldn't find {} environemnt variable", name)), + None, + ) + })?; + let url = url::Url::parse(&value).map_err(|_| { + Error::new( + Kind::ConfigError(format!("Couldn't parse the contents of {} as an URL", name)), + None, + ) + })?; + let db_type = match url.scheme() { + "mysql" => ConfigDbType::Mysql, + "postgres" => ConfigDbType::Postgres, + "sqlite" => ConfigDbType::Sqlite, + _ => { + return Err(Error::new( + Kind::ConfigError(format!("Unsupported database")), + None, + )) + } + }; + Ok(Self { + main: Main { + db_type, + db_path: Some( + url.as_str()[url.scheme().len()..] + .trim_start_matches(':') + .trim_start_matches("//") + .to_string() + .into(), + ), + db_host: url.host_str().map(|r| r.to_string()), + db_port: url.port().map(|r| r.to_string()), + db_user: Some(url.username().to_string()), + db_pass: url.password().map(|r| r.to_string()), + db_name: Some(url.path().trim_start_matches('/').to_string()), + }, + }) + } + /// create a new Config instance from a config file located on the file system pub fn from_file_location>(location: T) -> Result { let file = std::fs::read_to_string(&location).map_err(|err| { @@ -193,9 +237,8 @@ pub(crate) fn build_db_url(name: &str, config: &Config) -> String { #[cfg(test)] mod tests { - use super::{build_db_url, Config, Error, Kind}; + use super::{build_db_url, Config, Kind}; use std::io::Write; - use std::path::Path; #[test] fn returns_config_error_from_invalid_config_location() { @@ -276,4 +319,24 @@ mod tests { build_db_url("postgres", &config) ); } + + #[test] + fn builds_db_env_var() { + std::env::set_var( + "DATABASE_URL", + "postgres://root:1234@localhost:5432/refinery", + ); + let config = Config::from_env_var("DATABASE_URL").unwrap(); + assert_eq!( + "postgres://root:1234@localhost:5432/refinery", + build_db_url("postgres", &config) + ); + } + + #[test] + fn builds_db_env_var_failure() { + std::env::set_var("DATABASE_URL", "this_is_not_an_url"); + let config = Config::from_env_var("DATABASE_URL"); + assert!(config.is_err()); + } } diff --git a/refinery_core/src/traits/mod.rs b/refinery_core/src/traits/mod.rs index 7f409d7c..b2f6acb5 100644 --- a/refinery_core/src/traits/mod.rs +++ b/refinery_core/src/traits/mod.rs @@ -94,7 +94,7 @@ pub(crate) const GET_LAST_APPLIED_MIGRATION_QUERY: &str = #[cfg(test)] mod tests { - use super::{check_missing_divergent, Error, Kind, Migration}; + use super::{check_missing_divergent, Kind, Migration}; fn get_migrations() -> Vec { let migration1 = Migration::unapplied(