feat: read and load configuration

This commit is contained in:
Aravinth Manivannan 2024-05-13 21:29:59 +05:30
parent df4c07344d
commit 5d56cf132c
Signed by: realaravinth
GPG key ID: F8F50389936984FF
6 changed files with 342 additions and 0 deletions

11
.env.docker-compose Normal file
View file

@ -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"

1
.env_sample Normal file
View file

@ -0,0 +1 @@
export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"

26
config/default.toml Normal file
View file

@ -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

77
src/settings/database.rs Normal file
View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<Self, ConfigError> {
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<DefaultState>) -> ConfigBuilder<DefaultState> {
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);
}
}

162
src/settings/mod.rs Normal file
View file

@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<Smtp>,
}
impl Settings {
fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
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<Self, ConfigError> {
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<DBType>
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>()?;
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;
}
}
}

65
src/settings/server.rs Normal file
View file

@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<DefaultState>) -> ConfigBuilder<DefaultState> {
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
);
}
}