Skip to content
Clone

feat(config): Added configuration loading and multi-homed service skeleton

ZhenYi committed
parent f413d80 commit 8a2a3ec Browse files

16 files changed

+497 −6

File tree 16
Added app/apihost/src/main.rs View app/apihost/src/main.rs
@@ -0,0 +1,3 @@
1fn main() {
2 println!("Hello, world!");
3}
Added app/apihost/Cargo.toml View app/apihost/Cargo.toml
@@ -0,0 +1,6 @@
1[package]
2name = "apihost"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
Added app/githost/src/main.rs View app/githost/src/main.rs
@@ -0,0 +1,3 @@
1fn main() {
2 println!("Hello, world!");
3}
Added app/githost/Cargo.toml View app/githost/Cargo.toml
@@ -0,0 +1,6 @@
1[package]
2name = "githost"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
Added app/synchost/src/main.rs View app/synchost/src/main.rs
@@ -0,0 +1,3 @@
1fn main() {
2 println!("Hello, world!");
3}
Added app/synchost/Cargo.toml View app/synchost/Cargo.toml
@@ -0,0 +1,6 @@
1[package]
2name = "synchost"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
Added config/default.toml View config/default.toml
@@ -0,0 +1,186 @@
1[s3]
2bucket = ""
3region = "us-east-1"
4endpoint = ""
5access_key_id = ""
6secret_access_key = ""
7path_style = false
8request_timeout_secs = 30
9
10[redis]
11url = "redis://localhost:6379"
12mode = "standalone"
13database = 0
14pool_size = 8
15connection_timeout_secs = 2
16username = ""
17password = ""
18
19[cache]
20l1_capacity = 10000
21l1_ttl_secs = 0
22l1_tti_secs = 0
23default_ttl_secs = 0
24backfill_enabled = true
25
26[cache.redis]
27url = "redis://localhost:6379"
28mode = "standalone"
29database = 0
30pool_size = 8
31connection_timeout_secs = 2
32username = ""
33password = ""
34
35[database]
36url = "postgres://postgres:postgres@localhost:5432/gitdata"
37host = ""
38port = 5432
39username = "postgres"
40password = ""
41database = "gitdata"
42schema_search_path = "public"
43application_name = "gitdata"
44ssl_mode = "prefer"
45max_connections = 16
46min_connections = 2
47connect_timeout_secs = 10
48idle_timeout_secs = 600
49acquire_timeout_secs = 10
50max_lifetime_secs = 1800
51sqlx_logging = false
52replicas = []
53
54[session]
55mode = "opt_in"
56cookie_name = "gitdata_session"
57cookie_domain = ""
58cookie_path = "/"
59cookie_http_only = true
60cookie_secure = false
61cookie_same_site = "lax"
62cookie_max_age_secs = 604800
63cookie_prefix_host = true
64cookie_encode_ip_and_user_agent = false
65lifespan_secs = 604800
66max_lifespan_secs = 2592000
67memory_lifespan_secs = 1800
68table_name = "session"
69key = ""
70database_key = ""
71always_save = false
72db_update_interval_secs = 60
73purge_database_update_secs = 86400
74enable_bloom_filter = true
75
76[pow]
77difficulty = 16
78max_difficulty = 24
79expires_secs = 60
80mac_key = ""
81
82[auth.password_policy]
83min_length = 8
84require_upper = true
85require_lower = true
86require_digit = true
87require_symbol = false
88
89[auth]
90verify_code_ttl_secs = 600
91verify_code_len = 6
92reset_token_ttl_secs = 1800
93reset_token_bytes = 32
94email_code_rate_limit_secs = 60
95max_verify_attempts = 5
96min_age = 13
97totp_encryption_key = ""
98
99[[auth.agreements]]
100key = "terms"
101version = "0.1.0"
102title = "Terms of Service"
103content_md = "Placeholder terms of service."
104
105[[auth.agreements]]
106key = "privacy"
107version = "0.1.0"
108title = "Privacy Policy"
109content_md = "Placeholder privacy policy."
110
111[web]
112web_base_url = "http://localhost:3000"
113
114[user]
115deletion_grace_days = 14
116email_code_rate_limit_secs = 60
117email_verify_code_ttl_secs = 900
118email_verify_max_attempts = 5
119email_verify_code_len = 6
120
121[space]
122max_pagination = 100
123default_pagination = 20
124max_space_name_len = 64
125max_space_desc_len = 512
126max_group_name_len = 64
127max_alias_name_len = 64
128max_invite_email_len = 254
129allow_self_join_groups = false
130
131[issues]
132max_pagination = 100
133default_pagination = 20
134max_board_name_len = 100
135max_title_len = 255
136max_body_len = 65535
137max_comment_len = 65535
138max_label_name_len = 50
139max_label_color_len = 7
140max_milestone_title_len = 100
141max_milestone_description_len = 1024
142max_reaction_emoji_len = 32
143max_mentions_per_message = 20
144
145[git]
146repo_root = "/opt/gitdata/repos"
147objects_root = "/opt/gitdata/objects"
148max_blob_size = 104857600
149git_command_timeout_secs = 300
150git_rpc_timeout_secs = 3600
151
152[git.sync]
153lock_ttl_secs = 120
154max_commits_per_sync = 5000
155concurrency = 4
156queue_topic = "gitdata:tasks"
157dlq_topic = "gitdata:dlq"
158max_lock_retries = 5
159max_languages = 20
160language_timeout_secs = 30
161brpop_timeout_secs = 5
162git_command_timeout_secs = 120
163
164[git.ssh]
165bind_addr = "0.0.0.0"
166port = 22
167host_key_path = "/etc/gitdata/ssh_host_ed25519"
168banner = ""
169max_sessions = 64
170auth_rejection_time = 2
171inactivity_timeout = 300
172keepalive_interval = 30
173keepalive_max = 3
174
175[mailbaby]
176enabled = false
177channel = "rest"
178url = ""
179api_key = ""
180auth_scheme = "header"
181api_key_header = ""
182account = ""
183from_address = ""
184from_name = ""
185reply_to = ""
186request_timeout_secs = 30
No newline at end of file
Added lib/config/src/error.rs View lib/config/src/error.rs
@@ -0,0 +1,16 @@
1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum ConfigError {
5 #[error("failed to load .env file: {0}")]
6 DotEnv(#[from] dotenvy::Error),
7
8 #[error("config parse/merge error: {0}")]
9 Figment(#[from] figment::Error),
10
11 #[error("config I/O error: {0}")]
12 Io(#[from] std::io::Error),
13
14 #[error("config validation failed: {0}")]
15 Validation(String),
16}
No newline at end of file
Modified lib/config/src/git.rs View lib/config/src/git.rs
@@ -1,6 +1,30 @@
11use serde::Deserialize;
22use std::time::Duration;
33
4mod duration_secs {
5 use serde::{Deserialize, Deserializer};
6 use std::time::Duration;
7
8 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
9 where
10 D: Deserializer<'de>,
11 {
12 u64::deserialize(deserializer).map(Duration::from_secs)
13 }
14}
15
16mod opt_duration_secs {
17 use serde::{Deserialize, Deserializer};
18 use std::time::Duration;
19
20 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
21 where
22 D: Deserializer<'de>,
23 {
24 Option::<u64>::deserialize(deserializer).map(|o| o.map(Duration::from_secs))
25 }
26}
27
428#[derive(Clone, Debug, Deserialize)]
529#[serde(default)]
630pub struct GitConfig {
@@ -35,8 +59,11 @@
3559 pub host_key_path: String,
3660 pub banner: Option<String>,
3761 pub max_sessions: usize,
62 #[serde(deserialize_with = "duration_secs::deserialize")]
3863 pub auth_rejection_time: Duration,
64 #[serde(deserialize_with = "opt_duration_secs::deserialize")]
3965 pub inactivity_timeout: Option<Duration>,
66 #[serde(deserialize_with = "duration_secs::deserialize")]
4067 pub keepalive_interval: Duration,
4168 pub keepalive_max: u32,
4269}
Modified lib/config/src/lib.rs View lib/config/src/lib.rs
@@ -3,6 +3,7 @@
33use crate::database::DatabaseConfig;
44use crate::git::GitConfig;
55use crate::issues::IssueConfig;
6use crate::mailbaby::MailbabyConfig;
67use crate::pow::PowConfig;
78use crate::redis::RedisConfig;
89use crate::s3::S3Config;
@@ -27,6 +28,11 @@
2728pub mod issues;
2829pub mod git;
2930
31pub mod error;
32pub mod loader;
33
34pub use error::ConfigError;
35
3036#[derive(Clone, Debug, Default, Deserialize)]
3137#[serde(default)]
3238pub struct AppConfig {
@@ -42,4 +48,21 @@
4248 pub space: SpaceConfig,
4349 pub issues: IssueConfig,
4450 pub git: GitConfig,
51 pub mailbaby: MailbabyConfig,
52}
53
54impl AppConfig {
55 pub fn load() -> Result<Self, ConfigError> {
56 crate::loader::load_default()
57 }
58
59 pub fn validate(&self) -> Result<(), ConfigError> {
60 self.redis.validate().map_err(ConfigError::Validation)?;
61 self.database.validate().map_err(ConfigError::Validation)?;
62 self.pow.validate().map_err(ConfigError::Validation)?;
63 self.auth.validate().map_err(ConfigError::Validation)?;
64 self.cache.validate().map_err(ConfigError::Validation)?;
65 self.mailbaby.validate().map_err(ConfigError::Validation)?;
66 Ok(())
67 }
4568}
No newline at end of file
Added lib/config/src/loader.rs View lib/config/src/loader.rs
@@ -0,0 +1,37 @@
1use std::path::Path;
2
3use figment::{Figment, providers::{Env, Format, Toml}};
4
5use crate::error::ConfigError;
6use crate::AppConfig;
7
8pub const ENV_PREFIX: &str = "APP_";
9pub const ENV_SEPARATOR: &str = "__";
10pub const DEFAULT_CONFIG_PATH: &str = "config/default.toml";
11pub const LOCAL_CONFIG_PATH: &str = "config/local.toml";
12
13pub fn load_default() -> Result<AppConfig, ConfigError> {
14 let _ = dotenvy::dotenv();
15
16 let mut figment = Figment::new();
17 if Path::new(DEFAULT_CONFIG_PATH).exists() {
18 figment = figment.merge(Toml::file(DEFAULT_CONFIG_PATH));
19 }
20 if Path::new(LOCAL_CONFIG_PATH).exists() {
21 figment = figment.merge(Toml::file(LOCAL_CONFIG_PATH));
22 }
23 figment = figment.merge(Env::prefixed(ENV_PREFIX).split(ENV_SEPARATOR));
24
25 let cfg: AppConfig = figment.extract().map_err(ConfigError::from)?;
26 cfg.validate()?;
27 Ok(cfg)
28}
29
30pub fn load_from_env() -> Result<AppConfig, ConfigError> {
31 let _ = dotenvy::dotenv();
32 let figment = Figment::new()
33 .merge(Env::prefixed(ENV_PREFIX).split(ENV_SEPARATOR));
34 let cfg: AppConfig = figment.extract().map_err(ConfigError::from)?;
35 cfg.validate()?;
36 Ok(cfg)
37}
No newline at end of file
Modified lib/config/Cargo.toml View lib/config/Cargo.toml
@@ -7,3 +7,7 @@
77serde = { workspace = true, features = ["derive"] }
88sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
99base64 = { workspace = true }
10figment = { workspace = true }
11dotenvy = { workspace = true }
12thiserror = { workspace = true }
13tracing = { workspace = true }
Modified lib/export/src/lib.rs View lib/export/src/lib.rs
@@ -20,4 +20,29 @@
2020 pub storage: Arc<AppStorage>,
2121 pub email: AppEmail,
2222 pub config: AppConfig,
23}
24
25impl AppState {
26 pub async fn load() -> Result<Self, ExportError> {
27 let config = config::AppConfig::load()
28 .map_err(|e| ExportError::InvalidConfig(e.to_string()))?;
29 Self::with_config(config).await
30 }
31
32 pub async fn with_config(config: AppConfig) -> Result<Self, ExportError> {
33 config
34 .validate()
35 .map_err(|e| ExportError::InvalidConfig(e.to_string()))?;
36 let database = Arc::new(AppDatabase::connect(config.database.clone()).await?);
37 let redis = AppRedis::connect(config.redis.clone()).await?;
38 let storage = Arc::new(AppStorage::connect(config.s3.clone())?);
39 let email = AppEmail::connect(config.mailbaby.clone()).await?;
40 Ok(Self {
41 database,
42 redis,
43 storage,
44 email,
45 config,
46 })
47 }
2348}
No newline at end of file
Modified .gitignore View .gitignore
@@ -1,4 +1,6 @@
11**.iml
22.idea
33target
4.zcode
No newline at end of file
4.zcode
5config/local.toml
6.env
No newline at end of file
Modified Cargo.lock View Cargo.lock
@@ -154,6 +154,10 @@
154154checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
155155
156156[[package]]
157name = "apihost"
158version = "1.0.0"
159
160[[package]]
157161name = "ar_archive_writer"
158162version = "0.5.3"
159163source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -528,6 +532,15 @@
528532]
529533
530534[[package]]
535name = "atomic"
536version = "0.6.1"
537source = "registry+https://github.com/rust-lang/crates.io-index"
538checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
539dependencies = [
540 "bytemuck",
541]
542
543[[package]]
531544name = "atomic-waker"
532545version = "1.1.2"
533546source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -961,6 +974,12 @@
961974]
962975
963976[[package]]
977name = "bytemuck"
978version = "1.25.2"
979source = "registry+https://github.com/rust-lang/crates.io-index"
980checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
981
982[[package]]
964983name = "byteorder"
965984version = "1.5.0"
966985source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1142,8 +1161,12 @@
11421161version = "1.0.0"
11431162dependencies = [
11441163 "base64 0.23.1",
1164 "dotenvy",
1165 "figment",
11451166 "sea-orm",
11461167 "serde",
1168 "thiserror 2.0.20",
1169 "tracing",
11471170]
11481171
11491172[[package]]
@@ -2035,6 +2058,20 @@
20352058checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
20362059
20372060[[package]]
2061name = "figment"
2062version = "0.10.19"
2063source = "registry+https://github.com/rust-lang/crates.io-index"
2064checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
2065dependencies = [
2066 "atomic",
2067 "pear",
2068 "serde",
2069 "toml",
2070 "uncased",
2071 "version_check 0.9.5",
2072]
2073
2074[[package]]
20382075name = "filetime"
20392076version = "0.2.29"
20402077source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2525,6 +2562,10 @@
25252562]
25262563
25272564[[package]]
2565name = "githost"
2566version = "1.0.0"
2567
2568[[package]]
25282569name = "gix"
25292570version = "0.86.0"
25302571source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4415,6 +4456,12 @@
44154456]
44164457
44174458[[package]]
4459name = "inlinable_string"
4460version = "0.1.15"
4461source = "registry+https://github.com/rust-lang/crates.io-index"
4462checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
4463
4464[[package]]
44184465name = "inout"
44194466version = "0.1.4"
44204467source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5558,6 +5605,29 @@
55585605]
55595606
55605607[[package]]
5608name = "pear"
5609version = "0.2.9"
5610source = "registry+https://github.com/rust-lang/crates.io-index"
5611checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
5612dependencies = [
5613 "inlinable_string",
5614 "pear_codegen",
5615 "yansi",
5616]
5617
5618[[package]]
5619name = "pear_codegen"
5620version = "0.2.9"
5621source = "registry+https://github.com/rust-lang/crates.io-index"
5622checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
5623dependencies = [
5624 "proc-macro2",
5625 "proc-macro2-diagnostics",
5626 "quote",
5627 "syn 2.0.119",
5628]
5629
5630[[package]]
55615631name = "pem-rfc7468"
55625632version = "0.7.0"
55635633source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5910,7 +5980,7 @@
59105980source = "registry+https://github.com/rust-lang/crates.io-index"
59115981checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
59125982dependencies = [
5913 "toml_edit",
5983 "toml_edit 0.25.13+spec-1.1.0",
59145984]
59155985
59165986[[package]]
@@ -7112,6 +7182,15 @@
71127182]
71137183
71147184[[package]]
7185name = "serde_spanned"
7186version = "0.6.9"
7187source = "registry+https://github.com/rust-lang/crates.io-index"
7188checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
7189dependencies = [
7190 "serde",
7191]
7192
7193[[package]]
71157194name = "serde_stacker"
71167195version = "0.1.14"
71177196source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7859,6 +7938,10 @@
78597938]
78607939
78617940[[package]]
7941name = "synchost"
7942version = "1.0.0"
7943
7944[[package]]
78627945name = "synstructure"
78637946version = "0.13.2"
78647947source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8105,7 +8188,28 @@
81058188]
81068189
81078190[[package]]
8191name = "toml"
8192version = "0.8.23"
8193source = "registry+https://github.com/rust-lang/crates.io-index"
8194checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
8195dependencies = [
8196 "serde",
8197 "serde_spanned",
8198 "toml_datetime 0.6.11",
8199 "toml_edit 0.22.27",
8200]
8201
8202[[package]]
81088203name = "toml_datetime"
8204version = "0.6.11"
8205source = "registry+https://github.com/rust-lang/crates.io-index"
8206checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
8207dependencies = [
8208 "serde",
8209]
8210
8211[[package]]
8212name = "toml_datetime"
81098213version = "1.1.1+spec-1.1.0"
81108214source = "registry+https://github.com/rust-lang/crates.io-index"
81118215checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
@@ -8115,14 +8219,28 @@
81158219
81168220[[package]]
81178221name = "toml_edit"
8222version = "0.22.27"
8223source = "registry+https://github.com/rust-lang/crates.io-index"
8224checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
8225dependencies = [
8226 "indexmap",
8227 "serde",
8228 "serde_spanned",
8229 "toml_datetime 0.6.11",
8230 "toml_write",
8231 "winnow 0.7.15",
8232]
8233
8234[[package]]
8235name = "toml_edit"
81188236version = "0.25.13+spec-1.1.0"
81198237source = "registry+https://github.com/rust-lang/crates.io-index"
81208238checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
81218239dependencies = [
81228240 "indexmap",
8123 "toml_datetime",
8241 "toml_datetime 1.1.1+spec-1.1.0",
81248242 "toml_parser",
8125 "winnow",
8243 "winnow 1.0.4",
81268244]
81278245
81288246[[package]]
@@ -8131,10 +8249,16 @@
81318249source = "registry+https://github.com/rust-lang/crates.io-index"
81328250checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
81338251dependencies = [
8134 "winnow",
8252 "winnow 1.0.4",
81358253]
81368254
81378255[[package]]
8256name = "toml_write"
8257version = "0.1.2"
8258source = "registry+https://github.com/rust-lang/crates.io-index"
8259checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
8260
8261[[package]]
81388262name = "tonic"
81398263version = "0.14.6"
81408264source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8333,6 +8457,15 @@
83338457]
83348458
83358459[[package]]
8460name = "uncased"
8461version = "0.9.10"
8462source = "registry+https://github.com/rust-lang/crates.io-index"
8463checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
8464dependencies = [
8465 "version_check 0.9.5",
8466]
8467
8468[[package]]
83368469name = "unicase"
83378470version = "1.4.2"
83388471source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9004,6 +9137,15 @@
90049137
90059138[[package]]
90069139name = "winnow"
9140version = "0.7.15"
9141source = "registry+https://github.com/rust-lang/crates.io-index"
9142checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
9143dependencies = [
9144 "memchr",
9145]
9146
9147[[package]]
9148name = "winnow"
90079149version = "1.0.4"
90089150source = "registry+https://github.com/rust-lang/crates.io-index"
90099151checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
Modified Cargo.toml View Cargo.toml
@@ -11,7 +11,7 @@
1111 "lib/user",
1212 "lib/space",
1313 "lib/issues"
14, "lib/git", "lib/webapi"]
14, "lib/git", "lib/webapi", "app/apihost", "app/githost", "app/synchost"]
1515
1616resolver = "3"
1717
@@ -42,6 +42,8 @@
4242axum = "0.8.9"
4343axum_session = { version = "0.21.0", features = ["key-store"] }
4444tracing = "0.1.44"
45figment = { version = "0.10", features = ["toml", "env"] }
46dotenvy = "0.15"
4547rust-s3-async = "0.35.0"
4648redis = { version = "1.6.0", features = ["aio", "tokio-comp", "connection-manager", "cluster-async"] }
4749sea-orm = { version = "2.0.2", features = ["sqlx-postgres", "runtime-tokio-rustls"] }