Skip to content

Commit 51c3dc7

Browse files
committed
feat(market-server-rust)!: extend configuration
Extended the application configuration through a new Config structure. Database secrets can now be loaded from files. Also added a /health route for automated health check. Closes #45 Closes #51 BREAKING CHANGE: The DATABASE_URL arguments have been splitted into DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME
1 parent b6b2321 commit 51c3dc7

6 files changed

Lines changed: 305 additions & 81 deletions

File tree

docker-compose.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ services:
2626
context: ./servers/rust-server
2727
dockerfile: docker/Dockerfile
2828
environment:
29-
FINWAR_MARKET_DATABASE_URL: postgresql://${POSTGRES_USER:-finwar}:${POSTGRES_PASSWORD:-password}@timescaledb:5432/${POSTGRES_DB:-finwar}
30-
RUST_LOG: info
29+
FINWAR_MARKET_DB_USER: ${POSTGRES_USER:-finwar}
30+
FINWAR_MARKET_DB_PASSWORD: ${POSTGRES_PASSWORD:-password}
31+
FINWAR_MARKET_DB_HOST: timescaledb
32+
FINWAR_MARKET_DB_NAME: ${POSTGRES_DB:-finwar}
3133
command: migrate
3234
depends_on:
3335
timescaledb:
@@ -40,9 +42,11 @@ services:
4042
ports:
4143
- "4444:4444"
4244
environment:
43-
FINWAR_MARKET_DATABASE_URL: postgresql://${POSTGRES_USER:-finwar}:${POSTGRES_PASSWORD:-password}@timescaledb:5432/${POSTGRES_DB:-finwar}
44-
FINWAR_MARKET_TARGET_TICKER: NVDA
45-
RUST_LOG: info
45+
FINWAR_MARKET_DB_USER: ${POSTGRES_USER:-finwar}
46+
FINWAR_MARKET_DB_PASSWORD: ${POSTGRES_PASSWORD:-password}
47+
FINWAR_MARKET_DB_HOST: timescaledb
48+
FINWAR_MARKET_DB_NAME: ${POSTGRES_DB:-finwar}
49+
FINWAR_MARKET_TARGET_TICKER: AAPL
4650
command: serve
4751
depends_on:
4852
migration:

servers/rust-server/Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

servers/rust-server/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ sea-orm-cli migrate reset
4545

4646
### Environment Setup
4747

48-
- Set `DATABASE_URL=postgres://finwar:password@localhost/finwar` as an env variable or in a .env
48+
- Set `FINWAR_MARKET_DB_USER=finwar`, `FINWAR_MARKET_DB_PASSWORD=password`, and `FINWAR_MARKET_DB_NAME=finwar` as an env variable or in a .env
4949
- Default server runs on `0.0.0.0:4444`
5050
- Stock data loaded from `./local/data/Stocks/` directory
5151

@@ -68,7 +68,7 @@ docker build -t finwar-rust-server -f ./docker/Dockerfile .
6868
**Run only the server container:**
6969

7070
```bash
71-
docker run -p 4444:4444 -e DATABASE_URL=postgresql://finwar:password@host.docker.internal:5432/finwar finwar-rust-server
71+
docker run -p 4444:4444 -e FINWAR_MARKET_DB_USER=finwar FINWAR_MARKET_DB_PASSWORD=password FINWAR_MARKET_DB_NAME=finwar finwar-rust-server
7272
```
7373

7474
**Or use docker-compose (recommended):**

servers/rust-server/src/cli.rs

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,97 @@
11
use clap::{Parser, Subcommand};
2-
use migration::{Migrator, MigratorTrait};
2+
use std::path::PathBuf;
33

4-
/// Simple CLI for the market server. Defaults to `serve` when no subcommand
5-
/// is provided.
6-
#[derive(Debug, Parser)]
7-
#[command(author, version, about = "Finwar market server CLI")]
8-
pub struct Opts {
9-
#[command(subcommand)]
10-
pub command: Command,
4+
macro_rules! env_prefix {
5+
($name:expr) => {
6+
concat!("FINWAR_MARKET_", $name)
7+
};
118
}
129

13-
#[derive(Debug, Subcommand)]
14-
pub enum Command {
15-
/// Start the HTTP server (default)
16-
Serve,
17-
/// Run database migrations using the workspace `migration` member
18-
Migrate,
10+
/// Simple CLI for the market server.
11+
#[derive(Debug, Parser, Clone)]
12+
#[command(author, version, about = "Finwar market server CLI")]
13+
pub struct Args {
14+
/// Log level (trace, debug, info, warn, error)
15+
#[arg(long, env = env_prefix!("LOG_LEVEL"), default_value = "info")]
16+
pub log_level: String,
17+
18+
#[command(subcommand)]
19+
pub command: Commands,
1920
}
2021

21-
pub async fn run() -> Result<(), crate::error::Error> {
22-
let opts = Opts::parse();
22+
#[derive(Subcommand, Debug, Clone)]
23+
pub enum Commands {
24+
Serve {
25+
/// Host to bind to
26+
#[arg(long, env = env_prefix!("HOST"), default_value = "0.0.0.0")]
27+
host: String,
28+
29+
/// Port to bind to
30+
#[arg(short, long, env = env_prefix!("PORT"), default_value = "4444")]
31+
port: u16,
32+
33+
/// Database user
34+
#[arg(long, env = env_prefix!("DB_USER"), required_unless_present = "db_user_file")]
35+
db_user: Option<String>,
36+
37+
/// Database user file path
38+
#[arg(long, env = env_prefix!("DB_USER_FILE"), required_unless_present = "db_user")]
39+
db_user_file: Option<PathBuf>,
40+
41+
/// Database password
42+
#[arg(long, env = env_prefix!("DB_PASSWORD"), required_unless_present = "db_password_file")]
43+
db_password: Option<String>,
44+
45+
/// Database password file path
46+
#[arg(long, env = env_prefix!("DB_PASSWORD_FILE"), required_unless_present = "db_password")]
47+
db_password_file: Option<PathBuf>,
48+
49+
/// Database host
50+
#[arg(long, env = env_prefix!("DB_HOST"), default_value = "localhost")]
51+
db_host: String,
52+
53+
/// Database port
54+
#[arg(long, env = env_prefix!("DB_PORT"), default_value = "5432")]
55+
db_port: u16,
56+
57+
/// Database name
58+
#[arg(long, env = env_prefix!("DB_NAME"), default_value = "postgres")]
59+
db_name: String,
60+
61+
#[arg(long, env = env_prefix!("TARGET_SYMBOL"), default_value = "AAPL")]
62+
target_symbol: String,
63+
64+
#[arg(long, env = env_prefix!("TICK_INTERVAL_SECONDS"), default_value = "60")]
65+
tick_interval_seconds: u64
66+
},
2367

24-
match opts.command {
25-
Command::Serve => {
26-
let database_url =
27-
std::env::var("FINWAR_MARKET_DATABASE_URL")
28-
.expect("FINWAR_MARKET_DATABASE_URL must be set");
68+
Migrate {
69+
/// Database user
70+
#[arg(long, env = env_prefix!("DB_USER"), required_unless_present = "db_user_file")]
71+
db_user: Option<String>,
2972

30-
let db_connection = sea_orm::Database::connect(&database_url)
31-
.await
32-
.map_err(crate::error::Error::InitDb)?;
73+
/// Database user file path
74+
#[arg(long, env = env_prefix!("DB_USER_FILE"), required_unless_present = "db_user")]
75+
db_user_file: Option<PathBuf>,
3376

34-
crate::run_server(db_connection).await
35-
},
36-
Command::Migrate => {
37-
let database_url =
38-
std::env::var("FINWAR_MARKET_DATABASE_URL")
39-
.expect("FINWAR_MARKET_DATABASE_URL must be set");
77+
/// Database password
78+
#[arg(long, env = env_prefix!("DB_PASSWORD"), required_unless_present = "db_password_file")]
79+
db_password: Option<String>,
4080

41-
let db_connection = sea_orm::Database::connect(&database_url)
42-
.await
43-
.map_err(crate::error::Error::InitDb)?;
81+
/// Database password file path
82+
#[arg(long, env = env_prefix!("DB_PASSWORD_FILE"), required_unless_present = "db_password")]
83+
db_password_file: Option<PathBuf>,
4484

45-
Migrator::up(&db_connection, None).await?;
85+
/// Database host
86+
#[arg(long, env = env_prefix!("DB_HOST"), default_value = "localhost")]
87+
db_host: String,
88+
89+
/// Database port
90+
#[arg(long, env = env_prefix!("DB_PORT"), default_value = "5432")]
91+
db_port: u16,
4692

47-
Ok(())
48-
},
93+
/// Database name
94+
#[arg(long, env = env_prefix!("DB_NAME"), default_value = "postgres")]
95+
db_name: String,
4996
}
5097
}

servers/rust-server/src/config.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use crate::cli::{Args, Commands};
2+
use std::fs;
3+
4+
#[derive(Debug, Clone)]
5+
pub struct Config {
6+
pub server: ServerConfig,
7+
pub database: DatabaseConfig,
8+
pub log: LogConfig,
9+
pub target_symbol: String,
10+
pub tick_interval_seconds: u64,
11+
}
12+
13+
#[derive(Debug, Clone)]
14+
pub struct ServerConfig {
15+
pub host: String,
16+
pub port: u16,
17+
}
18+
19+
#[derive(Debug, Clone)]
20+
pub struct DatabaseConfig {
21+
pub user: Sensitive<String>,
22+
pub password: Sensitive<String>,
23+
pub host: String,
24+
pub port: u16,
25+
pub name: String,
26+
}
27+
28+
#[derive(Debug, Clone)]
29+
pub struct LogConfig {
30+
pub level: String,
31+
}
32+
33+
// Wrapper type for sensitive data that masks itself when printed
34+
#[derive(Clone)]
35+
pub struct Sensitive<T>(T);
36+
37+
impl<T> Sensitive<T> {
38+
fn new(value: T) -> Self {
39+
Sensitive(value)
40+
}
41+
42+
pub fn expose(&self) -> &T {
43+
&self.0
44+
}
45+
}
46+
47+
impl<T> std::fmt::Debug for Sensitive<T> {
48+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49+
write!(f, "***")
50+
}
51+
}
52+
53+
impl<T> std::fmt::Display for Sensitive<T> {
54+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55+
write!(f, "***")
56+
}
57+
}
58+
59+
impl Config {
60+
pub fn from_args(
61+
args: Args,
62+
) -> Self {
63+
match args.command {
64+
Commands::Serve {
65+
host,
66+
port,
67+
db_user,
68+
db_user_file,
69+
db_password,
70+
db_password_file,
71+
db_host,
72+
db_port,
73+
db_name,
74+
target_symbol,
75+
tick_interval_seconds,
76+
} => {
77+
Config {
78+
server: ServerConfig { host, port },
79+
database: DatabaseConfig {
80+
user: Sensitive::new(db_user.unwrap_or_else(|| {
81+
fs::read_to_string(db_user_file.expect("missing database user secret file path"))
82+
.expect("failed to read database user secret file").trim_end().to_string()
83+
})),
84+
password: Sensitive::new(db_password.unwrap_or_else(|| {
85+
fs::read_to_string(db_password_file.expect("missing database user secret file path"))
86+
.expect("failed to read database password secret file").trim_end().to_string()
87+
})),
88+
host: db_host,
89+
port: db_port,
90+
name: db_name,
91+
},
92+
log: LogConfig {
93+
level: args.log_level,
94+
},
95+
target_symbol: target_symbol,
96+
tick_interval_seconds: tick_interval_seconds,
97+
}
98+
},
99+
Commands::Migrate {
100+
db_user,
101+
db_user_file,
102+
db_password,
103+
db_password_file,
104+
db_host,
105+
db_port,
106+
db_name,
107+
} => {
108+
Config {
109+
server: ServerConfig { host: String::from(""), port: 0 },
110+
database: DatabaseConfig {
111+
user: Sensitive::new(db_user.unwrap_or_else(|| {
112+
fs::read_to_string(db_user_file.expect("missing database user secret file path"))
113+
.expect("failed to read database user secret file").trim_end().to_string()
114+
})),
115+
password: Sensitive::new(db_password.unwrap_or_else(|| {
116+
fs::read_to_string(db_password_file.expect("missing database user secret file path"))
117+
.expect("failed to read database password secret file").trim_end().to_string()
118+
})),
119+
host: db_host,
120+
port: db_port,
121+
name: db_name,
122+
},
123+
log: LogConfig {
124+
level: args.log_level,
125+
},
126+
target_symbol: String::from(""),
127+
tick_interval_seconds: 0,
128+
}
129+
}
130+
}
131+
132+
}
133+
}

0 commit comments

Comments
 (0)