From 5d56cf132c813d04722abcad5f88e0a74e93f730 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Mon, 13 May 2024 21:29:59 +0530 Subject: [PATCH] feat: read and load configuration --- .env.docker-compose | 11 +++ .env_sample | 1 + config/default.toml | 26 +++++++ src/settings/database.rs | 77 +++++++++++++++++++ src/settings/mod.rs | 162 +++++++++++++++++++++++++++++++++++++++ src/settings/server.rs | 65 ++++++++++++++++ 6 files changed, 342 insertions(+) create mode 100644 .env.docker-compose create mode 100644 .env_sample create mode 100644 config/default.toml create mode 100644 src/settings/database.rs create mode 100644 src/settings/mod.rs create mode 100644 src/settings/server.rs diff --git a/.env.docker-compose b/.env.docker-compose new file mode 100644 index 0000000..9b7270c --- /dev/null +++ b/.env.docker-compose @@ -0,0 +1,11 @@ +VANIGAM_debug=true +VANIGAM_source_code="https://git.batsense.net/libre-solutions/vanigam" +VANIGAM_allow_registration=true + +#DATABASE_URL= +VANIGAM_database_POOL=4 + +PORT=7000 +VANIGAM_server_DOMAIN="localhost:7000" +#VANIGAM_server_COOKIE_SECRET= +VANIGAM_server_IP="127.0.0.1" diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..a5a10b0 --- /dev/null +++ b/.env_sample @@ -0,0 +1 @@ +export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..6980e45 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,26 @@ +debug = true +source_code = "https://git.batsense.net/libre-solutions/vanigam" +allow_registration = true + +[server] +# Please set a unique value, your mCaptcha instance's security depends on this being +# unique +#cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d" +# The port at which you want authentication to listen to +# takes a number, choose from 1000-10000 if you dont know what you are doing +port = 7000 +#IP address. Enter 0.0.0.0 to listen on all available addresses +ip= "0.0.0.0" +# enter your hostname, eg: example.com +domain = "localhost:7000" +#cookie_secret = "" + +[database] +# This section deals with the database location and how to access it +# Please note that at the moment, we have support for only postgresqa. +# Example, if you are Batman, your config would be: +# url = "postgres://batman:password@batcave.org:5432/batcave" +# database_type = "postgres" +# pool = 4 +url = "postgres://example.org" # hack for tests to run successfully +pool = 4 diff --git a/src/settings/database.rs b/src/settings/database.rs new file mode 100644 index 0000000..f5b2722 --- /dev/null +++ b/src/settings/database.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::env; + +use config::{builder::DefaultState, ConfigBuilder, ConfigError}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DBType { + #[display(fmt = "postgres")] + Postgres, +} + +impl DBType { + fn from_url(url: &Url) -> Result { + match url.scheme() { + // "mysql" => Ok(Self::Maria), + "postgres" => Ok(Self::Postgres), + _ => Err(ConfigError::Message("Unknown database type".into())), + } + } +} + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct Database { + pub url: String, + pub pool: u32, + pub database_type: DBType, +} + +impl Database { + pub fn env_override(mut s: ConfigBuilder) -> ConfigBuilder { + for (parameter, env_var_name) in [ + ("database.url", "DATABASE_URL"), + ("database.pool", "FORGEFLUX_database_POOL"), + ] + .iter() + { + if let Ok(val) = env::var(env_var_name) { + log::debug!("Overriding [{parameter}] with environment variable {env_var_name}"); + s = s.set_override(parameter, val).unwrap(); + } + } + + s + } + + pub fn set_database_type(&mut self) { + let url = + Url::parse(&self.url).expect("couldn't parse Database URL and detect database type"); + self.database_type = DBType::from_url(&url).unwrap(); + } +} + +#[cfg(test)] +mod tests { + use crate::env_helper; + + use super::*; + + #[test] + fn test_db_env_override() { + let init_settings = crate::settings::Settings::new().unwrap(); + env_helper!( + init_settings, + "DATABASE_URL", + "postgres://test_db_env_override", + database.url + ); + env_helper!(init_settings, "FORGEFLUX_database_POOL", 99, database.pool); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..4e34281 --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::path::Path; +use std::{env, fs}; + +use config::builder::DefaultState; +use config::{Config, ConfigBuilder, ConfigError, File}; + +use serde::Deserialize; +use url::Url; + +pub mod database; +pub mod server; + +use database::{DBType, Database}; +use server::Server; + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct Settings { + pub debug: bool, + pub log: String, + pub source_code: String, + pub allow_registration: bool, + pub database: Database, + pub server: Server, + // pub smtp: Option, +} + +impl Settings { + fn env_override(mut s: ConfigBuilder) -> ConfigBuilder { + for (parameter, env_var_name) in [ + ("debug", "VANIGAM_debug"), + ("source_code", "VANIGAM_source_code"), + ("allow_registration", "VANIGAM_allow_registration"), + ] + .iter() + { + if let Ok(val) = env::var(env_var_name) { + log::debug!("Overriding [{parameter}] with environment variable {env_var_name}"); + s = s.set_override(parameter, val).unwrap(); + } + } + + s = Database::env_override(s); + Server::env_override(s) + } + + pub fn new() -> Result { + let mut s = Config::builder(); + + const CURRENT_DIR: &str = "./config/default.toml"; + const ETC: &str = "/etc/forgeflux/config.toml"; + + // Will be overridden after config is parsed and loaded into Settings by + // Settings::set_database_type. + // This parameter is not ergonomic for users, but it is required and can be programatically + // inferred. But we need a default value for config lib to parse successfully, since it is + // DBType and not Option + s = s + .set_default("database.database_type", DBType::Postgres.to_string()) + .expect("unable to set database.database_type default config"); + + s = s + .set_default("log", "INFO") + .expect("unable to set log default config"); + + if let Ok(path) = env::var("VANIGAM_CONFIG") { + let absolute_path = Path::new(&path).canonicalize().unwrap(); + log::info!( + "Loading config file from {}", + absolute_path.to_str().unwrap() + ); + s = s.add_source(File::with_name(absolute_path.to_str().unwrap())); + } else if Path::new(CURRENT_DIR).exists() { + let absolute_path = fs::canonicalize(CURRENT_DIR).unwrap(); + log::info!( + "Loading config file from {}", + absolute_path.to_str().unwrap() + ); + // merging default config from file + s = s.add_source(File::with_name(absolute_path.to_str().unwrap())); + } else if Path::new(ETC).exists() { + log::info!("{}", format!("Loading config file from {}", ETC)); + s = s.add_source(File::with_name(ETC)); + } else { + log::warn!("Configuration file not found"); + } + + s = Self::env_override(s); + + let mut settings = s.build()?.try_deserialize::()?; + settings.check_url(); + + settings.database.set_database_type(); + + Ok(settings) + } + + fn check_url(&self) { + Url::parse(&self.source_code).expect("Please enter a URL for source_code in settings"); + } +} + +#[cfg(test)] +pub mod tests { + use url::Url; + + use super::Settings; + use crate::db::create_database::CreateDatabase; + use crate::db::delete_database::DeleteDatabase; + use crate::db::migrate::RunMigrations; + use crate::db::sqlx_postgres::Postgres; + use crate::utils::random_string::{GenerateRandomString, GenerateRandomStringInterface}; + + #[macro_export] + macro_rules! env_helper { + ($init_settings:ident, $env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => { + println!("Setting env var {} to {} for test", $env, $val); + env::set_var($env, $val); + { + let new_settings = $crate::settings::Settings::new().unwrap(); + assert_eq!(new_settings.$($param).+, $val_typed, "should match"); + assert_ne!(new_settings.$($param).+, $init_settings.$($param).+); + } + env::remove_var($env); + }; + + + ($init_settings:ident, $env:expr, $val:expr, $($param:ident).+) => { + env_helper!($init_settings, $env, $val.to_string(), $val, $($param).+); + }; + } + + pub async fn get_settings() -> Settings { + let mut settings = Settings::new().unwrap(); + let mut db_url = Url::parse(&settings.database.url).unwrap(); + db_url.set_path(&GenerateRandomString.get_random(12)); + settings.database.url = db_url.to_string(); + + crate::db::sqlx_postgres::PostgresDatabase + .create_database(&db_url) + .await; + let db = Postgres::new( + sqlx::postgres::PgPool::connect(&settings.database.url) + .await + .unwrap(), + ); + db.migrate().await; + + settings + } + + impl Settings { + pub async fn drop_db(&self) { + crate::db::sqlx_postgres::PostgresDatabase + .delete_database(&Url::parse(&self.database.url).unwrap()) + .await; + } + } +} diff --git a/src/settings/server.rs b/src/settings/server.rs new file mode 100644 index 0000000..20f4b2b --- /dev/null +++ b/src/settings/server.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use config::{builder::DefaultState, ConfigBuilder}; +use serde::Deserialize; +use std::env; + +#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] +pub struct Server { + pub port: u32, + pub domain: String, + pub ip: String, + pub cookie_secret: String, +} + +impl Server { + pub fn get_ip(&self) -> String { + format!("{}:{}", self.ip, self.port) + } + + pub fn env_override(mut s: ConfigBuilder) -> ConfigBuilder { + for (parameter, env_var_name) in [ + ("server.port", "PORT"), + ("server.domain", "VANIGAM_server_DOMAIN"), + ("server.cookie_secret", "VANIGAM_server_COOKIE_SECRET"), + ("server.ip", "VANIGAM_server_IP"), + ] + .iter() + { + if let Ok(val) = env::var(env_var_name) { + log::debug!("Overriding [{parameter}] with environment variable {env_var_name}"); + s = s.set_override(parameter, val).unwrap(); + } + } + + s + } +} + +#[cfg(test)] +mod tests { + use crate::env_helper; + + use super::*; + + #[test] + fn test_server_env_override() { + let init_settings = crate::settings::Settings::new().unwrap(); + env_helper!(init_settings, "PORT", 22, server.port); + env_helper!( + init_settings, + "VANIGAM_server_DOMAIN", + "test_server_env_override.org", + server.domain + ); + env_helper!(init_settings, "VANIGAM_server_IP", "1.1.1.1", server.ip); + env_helper!( + init_settings, + "VANIGAM_server_COOKIE_SECRET", + "asdfasdflkjhasdlkfhalksdf", + server.cookie_secret + ); + } +}