feat: read and load app settings

This commit is contained in:
Aravinth Manivannan 2024-05-04 22:47:36 +05:30
parent 5637eb8415
commit 27204cd53d
Signed by: realaravinth
GPG Key ID: F8F50389936984FF
10 changed files with 3851 additions and 0 deletions

1
.env_sample Normal file
View File

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

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
.env

3419
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,32 @@ name = "forgeflux"
version = "0.1.0"
edition = "2021"
[workspace]
exclude = ["utils/db-migrations"] #, "utils/cache-bust"]
memebers = ["."]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4.5.1"
async-trait = "0.1.80"
chrono = "0.4.38"
config = "0.14.0"
derive_more = "0.99.17"
lazy_static = "1.4.0"
log = "0.4.21"
pretty_env_logger = "0.5.0"
rand = "0.8.5"
reqwest = { version = "0.12.4", features = ["json"] }
rust-embed = { version = "8.3.0", features = ["include-exclude"] }
serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0.116"
sqlx = { version = "0.7.4", features = ["runtime-tokio-rustls", "postgres", "time"] }
tera = "1.19.1"
tracing = { version = "0.1.40", features = ["log"] }
tracing-actix-web = "0.7.10"
url = { version = "2.5.0", features = ["serde"] }
[dev-dependencies]
actix-rt= "2.9"

30
config/default.toml Normal file
View File

@ -0,0 +1,30 @@
debug = true
source_code = "https://git.batsense.net/ForgeFlux/ForgeFlux"
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"
[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
[forges.forgejo]
client_id = "foo"
client_secret = "bar"
url = "http://example.org"

73
src/settings/database.rs Normal file
View File

@ -0,0 +1,73 @@
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);
}
}

View File

@ -0,0 +1,67 @@
use std::env;
use config::{builder::DefaultState, ConfigBuilder};
use serde::Deserialize;
use url::Url;
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct Forgejo {
pub url: Url,
pub client_id: String,
pub client_secret: String,
}
impl Forgejo {
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
for (parameter, env_var_name) in [
("forges.forgejo.url", "FORGEFLUX_forges_FORGEJO_url"),
(
"forges.forgejo.client_id",
"FORGEFLUX_forges_FORGEJO_client_id",
),
(
"forges.forgejo.client_secret",
"FORGEFLUX_forges_FORGEJO_client_secret",
),
]
.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_forges_forgejo_env_override() {
let init_settings = crate::settings::Settings::new().unwrap();
env_helper!(
init_settings,
"FORGEFLUX_forges_FORGEJO_url",
Url::parse("postgres://test_forges_forgejo_env_override").unwrap(),
forges.forgejo.url
);
env_helper!(
init_settings,
"FORGEFLUX_forges_FORGEJO_client_id",
"test_forges_forgejo_env_override_client_id",
forges.forgejo.client_id
);
env_helper!(
init_settings,
"FORGEFLUX_forges_FORGEJO_client_secret",
"test_forges_forgejo_env_override_client_secret",
forges.forgejo.client_secret
);
}
}

View File

@ -0,0 +1,18 @@
use config::{builder::DefaultState, ConfigBuilder};
use serde::Deserialize;
use std::env;
pub mod forgejo;
use forgejo::Forgejo;
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct Forges {
pub forgejo: Forgejo,
}
impl Forges {
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
Forgejo::env_override(s)
}
}

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

@ -0,0 +1,162 @@
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 forges;
pub mod server;
use database::{DBType, Database};
use forges::Forges;
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 forges: Forges,
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", "FORGEFLUX_debug"),
("source_code", "FORGEFLUX_source_code"),
("allow_registration", "FORGEFLUX_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);
s = Forges::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("FORGEFLUX_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::get_random;
#[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(&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;
}
}
}

54
src/settings/server.rs Normal file
View File

@ -0,0 +1,54 @@
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,
}
impl Server {
#[cfg(not(tarpaulin_include))]
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", "FORGEFLUX_server_DOMAIN"),
("server.ip", "FORGEFLUX_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,
"FORGEFLUX_server_DOMAIN",
"test_server_env_override.org",
server.domain
);
env_helper!(init_settings, "FORGEFLUX_server_IP", "1.1.1.1", server.ip);
}
}