Skip to content
Clone

feat(config): introduce scoped configuration for various components

ZhenYi committed
parent 99f5830 commit 8d21ff1 Browse files

10 files changed

+244 −22

File tree 10
Modified app/ai/src/main.rs View app/ai/src/main.rs
@@ -1,4 +1,5 @@
11use anyhow::Context;
2use config::scoped::{AiConfig, ScopedConfig};
23use tracing::{info, warn};
34
45mod worker;
@@ -7,14 +8,13 @@
78async fn main() -> anyhow::Result<()> {
89 init_tracing();
910
10 let cfg = config::AppConfig::load().context("loading configuration")?;
11 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
11 let cfg = AiConfig::load().context("loading configuration")?;
1212 anyhow::ensure!(cfg.agent.is_enabled(), "agent configuration is not enabled");
1313 anyhow::ensure!(cfg.nats.is_enabled(), "nats url must be set");
1414
1515 info!("connecting to database / redis / storage / nats");
1616 let queue = cfg.agent.queue.clone();
17 let state = export::AppState::with_config(cfg.clone())
17 let state = export::AppState::with_config(cfg.clone().into_app_config())
1818 .await
1919 .context("building app state")?;
2020 let runtime =
Modified app/apihost/src/main.rs View app/apihost/src/main.rs
@@ -2,6 +2,7 @@
22
33use anyhow::Context;
44use axum::Router;
5use config::scoped::{ApihostConfig, ScopedConfig};
56use tokio::net::TcpListener;
67use tracing::{info, warn};
78use utoipa_swagger_ui::SwaggerUi;
@@ -11,15 +12,14 @@
1112async fn main() -> anyhow::Result<()> {
1213 init_tracing();
1314
14 let cfg = config::AppConfig::load().context("loading configuration")?;
15 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
15 let cfg = ApihostConfig::load().context("loading configuration")?;
1616 if !cfg.apihost.enabled() {
1717 info!("apihost is disabled by config; exiting");
1818 return Ok(());
1919 }
2020
2121 info!("connecting to database / redis / storage / email");
22 let state = export::AppState::with_config(cfg.clone())
22 let state = export::AppState::with_config(cfg.clone().into_app_config())
2323 .await
2424 .context("building AppState")?;
2525 info!("dependencies ready");
Modified app/githost/src/main.rs View app/githost/src/main.rs
@@ -2,6 +2,7 @@
22use std::sync::Arc;
33
44use anyhow::Context;
5use config::scoped::{GithostConfig, ScopedConfig};
56use tokio::net::TcpListener;
67use tracing::{info, warn};
78
@@ -9,15 +10,14 @@
910async fn main() -> anyhow::Result<()> {
1011 init_tracing();
1112
12 let cfg = config::AppConfig::load().context("loading configuration")?;
13 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
13 let cfg = GithostConfig::load().context("loading configuration")?;
1414 if !cfg.githost.enabled() {
1515 info!("githost is disabled by config; exiting");
1616 return Ok(());
1717 }
1818
1919 info!("connecting to database / redis / storage / email");
20 let state = export::AppState::with_config(cfg.clone())
20 let state = export::AppState::with_config(cfg.clone().into_app_config())
2121 .await
2222 .context("building AppState")?;
2323 info!("dependencies ready");
Modified app/mails/src/main.rs View app/mails/src/main.rs
@@ -1,6 +1,7 @@
11use std::time::Duration;
22
33use anyhow::Context;
4use config::scoped::{MailsConfig, ScopedConfig};
45use email::runtime::{WorkerOptions, run_until_shutdown};
56use tracing::info;
67
@@ -8,8 +9,7 @@
89async fn main() -> anyhow::Result<()> {
910 init_tracing();
1011
11 let cfg = config::AppConfig::load().context("loading configuration")?;
12 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
12 let cfg = MailsConfig::load().context("loading configuration")?;
1313
1414 let redis_url = cfg.redis.url().to_string();
1515 let redis_host = match redis_url.rsplit_once('@') {
Modified app/migrate/src/main.rs View app/migrate/src/main.rs
@@ -1,5 +1,5 @@
11use anyhow::Context;
2use config::AppConfig;
2use config::scoped::{MigrateConfig, ScopedConfig};
33use models::migrate::sync_schema;
44use sea_orm::Database;
55use tracing::info;
@@ -15,8 +15,7 @@
1515 )
1616 .init();
1717
18 let cfg = AppConfig::load().context("loading configuration")?;
19 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
18 let cfg = MigrateConfig::load().context("loading configuration")?;
2019
2120 let opts = cfg
2221 .database
Modified app/synchost/src/main.rs View app/synchost/src/main.rs
@@ -3,6 +3,7 @@
33
44use anyhow::Context;
55use cache::CacheClient;
6use config::scoped::{ScopedConfig, SynchostConfig};
67use git::sync::{NoopWebhookTx, SyncQueue, SyncWorker, WebhookTx};
78use tokio_util::sync::CancellationToken;
89use tracing::{info, warn};
@@ -11,8 +12,7 @@
1112async fn main() -> anyhow::Result<()> {
1213 init_tracing();
1314
14 let cfg = config::AppConfig::load().context("loading configuration")?;
15 cfg.validate().map_err(|e| anyhow::anyhow!(e.to_string()))?;
15 let cfg = SynchostConfig::load().context("loading configuration")?;
1616
1717 info!("connecting to database / redis / cache");
1818 let db = Arc::new(
Modified lib/config/src/lib.rs View lib/config/src/lib.rs
@@ -14,6 +14,7 @@
1414pub mod quota;
1515pub mod redis;
1616pub mod s3;
17pub mod scoped;
1718pub mod session;
1819pub mod space;
1920pub mod user;
Modified lib/config/src/loader.rs View lib/config/src/loader.rs
@@ -13,7 +13,7 @@
1313pub const DEFAULT_CONFIG_PATH: &str = "config/default.toml";
1414pub const LOCAL_CONFIG_PATH: &str = "config/local.toml";
1515
16pub fn load_default() -> Result<AppConfig, ConfigError> {
16pub fn figment() -> Figment {
1717 let _ = dotenvy::dotenv();
1818
1919 let mut figment = Figment::new();
@@ -23,9 +23,11 @@
2323 if Path::new(LOCAL_CONFIG_PATH).exists() {
2424 figment = figment.merge(Toml::file(LOCAL_CONFIG_PATH));
2525 }
26 figment = figment.merge(Env::prefixed(ENV_PREFIX).split(ENV_SEPARATOR));
26 figment.merge(Env::prefixed(ENV_PREFIX).split(ENV_SEPARATOR))
27}
2728
28 let cfg: AppConfig = figment.extract().map_err(ConfigError::from)?;
29pub fn load_default() -> Result<AppConfig, ConfigError> {
30 let cfg: AppConfig = figment().extract().map_err(ConfigError::from)?;
2931 cfg.validate()?;
3032 Ok(cfg)
3133}
Added lib/config/src/scoped.rs View lib/config/src/scoped.rs
@@ -0,0 +1,223 @@
1use serde::Deserialize;
2use serde::de::DeserializeOwned;
3
4use crate::agent::AgentConfig;
5use crate::apihost::ApihostConfig as ApihostSection;
6use crate::auth::AuthConfig;
7use crate::cache::CacheConfig;
8use crate::chat::ChatConfig;
9use crate::database::DatabaseConfig;
10use crate::email::EmailConfig;
11use crate::email_queue::EmailQueueConfig;
12use crate::error::ConfigError;
13use crate::git::GitConfig;
14use crate::githost::GithostConfig as GithostSection;
15use crate::issues::IssueConfig;
16use crate::nats::NatsConfig;
17use crate::pow::PowConfig;
18use crate::quota::QuotaConfig;
19use crate::redis::RedisConfig;
20use crate::s3::S3Config;
21use crate::session::SessionConfig;
22use crate::space::SpaceConfig;
23use crate::user::UserConfig;
24use crate::web::WebConfig;
25use crate::{AppConfig, loader};
26
27pub trait ScopedConfig: DeserializeOwned + Sized {
28 fn validate_scoped(&self) -> Result<(), String>;
29
30 fn load() -> Result<Self, ConfigError> {
31 let cfg: Self = loader::figment().extract().map_err(ConfigError::from)?;
32 cfg.validate_scoped().map_err(ConfigError::Validation)?;
33 Ok(cfg)
34 }
35}
36
37#[derive(Clone, Debug, Default, Deserialize)]
38#[serde(default)]
39pub struct MigrateConfig {
40 pub database: DatabaseConfig,
41}
42
43impl ScopedConfig for MigrateConfig {
44 fn validate_scoped(&self) -> Result<(), String> {
45 self.database.validate()
46 }
47}
48
49#[derive(Clone, Debug, Default, Deserialize)]
50#[serde(default)]
51pub struct MailsConfig {
52 pub redis: RedisConfig,
53 pub email: EmailConfig,
54 pub email_queue: EmailQueueConfig,
55}
56
57impl ScopedConfig for MailsConfig {
58 fn validate_scoped(&self) -> Result<(), String> {
59 self.redis.validate()?;
60 self.email.is_valid()?;
61 self.email_queue.is_valid()
62 }
63}
64
65#[derive(Clone, Debug, Default, Deserialize)]
66#[serde(default)]
67pub struct SynchostConfig {
68 pub database: DatabaseConfig,
69 pub redis: RedisConfig,
70 pub cache: CacheConfig,
71 pub git: GitConfig,
72}
73
74impl ScopedConfig for SynchostConfig {
75 fn validate_scoped(&self) -> Result<(), String> {
76 self.database.validate()?;
77 self.redis.validate()?;
78 self.cache.validate()
79 }
80}
81
82// s3/email/email_queue 由 AppState 构建时无条件消费(EmailConfig 缺省视为启用、
83// S3Client 对空 bucket 报错),即使业务路径不用也必须入域,否则装配即失败
84#[derive(Clone, Debug, Default, Deserialize)]
85#[serde(default)]
86pub struct AiConfig {
87 pub database: DatabaseConfig,
88 pub redis: RedisConfig,
89 pub s3: S3Config,
90 pub email: EmailConfig,
91 pub email_queue: EmailQueueConfig,
92 pub nats: NatsConfig,
93 pub agent: AgentConfig,
94}
95
96impl AiConfig {
97 pub fn into_app_config(self) -> AppConfig {
98 AppConfig {
99 database: self.database,
100 redis: self.redis,
101 s3: self.s3,
102 email: self.email,
103 email_queue: self.email_queue,
104 nats: self.nats,
105 agent: self.agent,
106 ..Default::default()
107 }
108 }
109}
110
111impl ScopedConfig for AiConfig {
112 fn validate_scoped(&self) -> Result<(), String> {
113 self.database.validate()?;
114 self.redis.validate()?;
115 self.email.is_valid()?;
116 self.email_queue.is_valid()?;
117 self.agent.is_valid()
118 }
119}
120
121#[derive(Clone, Debug, Default, Deserialize)]
122#[serde(default)]
123pub struct GithostConfig {
124 pub database: DatabaseConfig,
125 pub redis: RedisConfig,
126 pub s3: S3Config,
127 pub email: EmailConfig,
128 pub email_queue: EmailQueueConfig,
129 pub cache: CacheConfig,
130 pub web: WebConfig,
131 pub git: GitConfig,
132 pub githost: GithostSection,
133}
134
135impl GithostConfig {
136 pub fn into_app_config(self) -> AppConfig {
137 AppConfig {
138 database: self.database,
139 redis: self.redis,
140 s3: self.s3,
141 email: self.email,
142 email_queue: self.email_queue,
143 cache: self.cache,
144 web: self.web,
145 git: self.git,
146 githost: self.githost,
147 ..Default::default()
148 }
149 }
150}
151
152impl ScopedConfig for GithostConfig {
153 fn validate_scoped(&self) -> Result<(), String> {
154 self.database.validate()?;
155 self.redis.validate()?;
156 self.cache.validate()?;
157 self.email.is_valid()?;
158 self.email_queue.is_valid()?;
159 self.githost.is_valid()
160 }
161}
162
163#[derive(Clone, Debug, Default, Deserialize)]
164#[serde(default)]
165pub struct ApihostConfig {
166 pub s3: S3Config,
167 pub redis: RedisConfig,
168 pub cache: CacheConfig,
169 pub chat: ChatConfig,
170 pub database: DatabaseConfig,
171 pub session: SessionConfig,
172 pub pow: PowConfig,
173 pub auth: AuthConfig,
174 pub web: WebConfig,
175 pub user: UserConfig,
176 pub quota: QuotaConfig,
177 pub space: SpaceConfig,
178 pub issues: IssueConfig,
179 pub git: GitConfig,
180 pub email: EmailConfig,
181 pub email_queue: EmailQueueConfig,
182 pub apihost: ApihostSection,
183}
184
185impl ApihostConfig {
186 pub fn into_app_config(self) -> AppConfig {
187 AppConfig {
188 s3: self.s3,
189 redis: self.redis,
190 cache: self.cache,
191 chat: self.chat,
192 database: self.database,
193 session: self.session,
194 pow: self.pow,
195 auth: self.auth,
196 web: self.web,
197 user: self.user,
198 quota: self.quota,
199 space: self.space,
200 issues: self.issues,
201 git: self.git,
202 email: self.email,
203 email_queue: self.email_queue,
204 apihost: self.apihost,
205 ..Default::default()
206 }
207 }
208}
209
210impl ScopedConfig for ApihostConfig {
211 fn validate_scoped(&self) -> Result<(), String> {
212 self.redis.validate()?;
213 self.database.validate()?;
214 self.pow.validate()?;
215 self.auth.validate()?;
216 self.cache.validate()?;
217 self.chat.is_valid()?;
218 self.email.is_valid()?;
219 self.email_queue.is_valid()?;
220 self.session.is_valid()?;
221 self.apihost.is_valid()
222 }
223}
Modified lib/export/src/lib.rs View lib/export/src/lib.rs
@@ -37,9 +37,6 @@
3737 }
3838
3939 pub async fn with_config(config: AppConfig) -> Result<Self, ExportError> {
40 config
41 .validate()
42 .map_err(|e| ExportError::InvalidConfig(e.to_string()))?;
4340 let database = Arc::new(AppDatabase::connect(config.database.clone()).await?);
4441 let redis = AppRedis::connect(config.redis.clone()).await?;
4542 let storage = Arc::new(AppStorage::connect(config.s3.clone())?);